-
-
Notifications
You must be signed in to change notification settings - Fork 35
One Time Authentication (OTA)
Since version 1.9.0, shinpuru implements a one time authentication system which allows simple login to the web interface via direct messages instead of logging in to the Discord account in the Browser to log in via OAuth2.
When a user executes the sp!login
command on any guild or directly via DM to shinpuru, a one time authentication token is generated. This token is a JWT containing the users ID as well as a unique character string which is cached in shinpuru together with the users ID. Also, the JWT stores the creation timestamp as well as a deadline timestamp after which the token is invalid. This JWT is then signed with a 128bit secret generated on startup of shinpuru using the HS256 algorithm.
The token generation looks like following:
func (a *OneTimeAuth) GetKey(userID string) (token string, err error) {
now := time.Now()
claims := otaClaims{}
claims.Issuer = fmt.Sprintf(jwtIssuer, util.AppVersion)
claims.Subject = userID
claims.ExpiresAt = now.Add(a.duration).Unix()
claims.NotBefore = now.Unix()
claims.IssuedAt = now.Unix()
if claims.Token, err = random.GetRandBase64Str(32); err != nil {
return
}
token, err = jwt.NewWithClaims(jwtGenerationMethod, claims).
SignedString(a.signingKey)
a.tokens.Set(claims.Token, userID, a.duration)
return
}
The deadline for each token is set to 60 seconds after creation of the token. After this deadline, the token validation fails and the stored string token bound to the user ID is cache-invalidated, so that you can not use any generated token after this 60 seconds period.
Now, you can login to the web interface via the GET https://<host>/ota?token=<token>
endpoint passing the previously generated token. You are automatically directed to this page by clicking the link in the message which shinpuru sends to your DM.
The shinpuru web server then tries to parse and validate the JWT and checks, if the included token string is included in the token cache on the on hand and if the token is linked to the same user ID as contained in the JWT on the other. If all these checks are valid and if the token is not expired yet, the default login routine is executed (generating session JWT and setting it as session cookie, i.e.).
This is the backend login behind the token validation:
func (a *OneTimeAuth) ValidateKey(key string) (userID string, err error) {
token, err := jwt.Parse(key, func(t *jwt.Token) (interface{}, error) {
return a.signingKey, nil
})
if err != nil {
return
}
if err = token.Claims.Valid(); err != nil {
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
err = errors.New("invalid claims")
return
}
userID, okS := claims["sub"].(string)
tkn, okT := claims["tkn"].(string)
if !okS || !okT {
err = errors.New("invalid claims")
return
}
if uid, ok := a.tokens.GetValue(tkn).(string); !ok || uid != userID {
err = errors.New("invalid token")
return
}
a.tokens.Remove(tkn)
return
}
After a successful authentication via OTA, the token string is removed from the cache, so that the token can not be used twice. Also, a final informational message is sent to the user which has been authenticated. This is only for security reasons and to double check if no one else has maliciously authenticated as you.
As discussed here, passing secrets via URL query might be insecure. Therefore, the token deadline window is set tightly to 60 seconds only and the token is invalidated after first usage.
Though, OTA is disabled by default. You need to log in to the web interface defaultly via OAuth2 to enable OTA in your user settings if you want to use this feature.