support linking OAuth 2.0 user to logged-in users

This commit is contained in:
MaysWind
2025-10-31 01:22:47 +08:00
parent 8a0777be4c
commit b690316aa7
24 changed files with 172 additions and 44 deletions
+26 -4
View File
@@ -488,11 +488,33 @@ func (a *AuthorizationsApi) OAuth2CallbackAuthorizeHandler(c *core.WebContext) (
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(c, user)
var token string
var claims *core.UserTokenClaims
if err != nil {
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
if credential.Token != "" {
_, claims, _, err = a.tokens.ParseToken(c, credential.Token)
if err != nil {
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to parse token, because %s", err.Error())
return nil, errs.ErrInvalidToken
}
if claims.Uid != user.Uid {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] oauth 2.0 user \"uid:%d\" does not match current user \"uid:%d\"", user.Uid, claims.Uid)
token = ""
claims = nil
} else {
token = credential.Token
}
}
if token == "" {
token, claims, err = a.tokens.CreateToken(c, user)
if err != nil {
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
}
c.SetTextualToken(token)
+60 -16
View File
@@ -73,6 +73,29 @@ func (a *OAuth2AuthenticationApi) LoginHandler(c *core.WebContext) (string, *err
return a.redirectToFailedCallbackPage(c, errs.ErrRepeatedRequest)
}
uid := int64(0)
if oauth2LoginReq.Token != "" {
_, claims, _, err := a.tokens.ParseToken(c, oauth2LoginReq.Token)
if err != nil {
log.Errorf(c, "[oauth2_authentications.LoginHandler] failed to parse token, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidToken)
}
uid = claims.Uid
user, err := a.users.GetUserById(c, uid)
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
log.Errorf(c, "[oauth2_authentications.LoginHandler] failed to get user by id %d, because %s", uid, err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_OAUTH2_LOGIN) {
return a.redirectToFailedCallbackPage(c, errs.ErrNotPermittedToPerformThisAction)
}
}
verifier, err := utils.GetRandomNumberOrLowercaseLetter(64)
if err != nil {
@@ -80,7 +103,7 @@ func (a *OAuth2AuthenticationApi) LoginHandler(c *core.WebContext) (string, *err
return a.redirectToFailedCallbackPage(c, errs.ErrSystemError)
}
remark = fmt.Sprintf("%s|%s|%s", oauth2LoginReq.Platform, oauth2LoginReq.ClientSessionId, verifier)
remark = fmt.Sprintf("%s|%s|%d|%s", oauth2LoginReq.Platform, oauth2LoginReq.ClientSessionId, uid, verifier)
state := fmt.Sprintf("%s|%s|%s", oauth2LoginReq.Platform, oauth2LoginReq.ClientSessionId, utils.MD5EncodeToString([]byte(remark)))
redirectUrl, err := oauth2.GetOAuth2AuthUrl(c, state, verifier)
@@ -123,7 +146,7 @@ func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *
stateParts := strings.Split(oauth2CallbackReq.State, "|")
if len(stateParts) >= 2 {
if len(stateParts) == 3 {
platform = stateParts[0]
clientSessionId = stateParts[1]
} else {
@@ -143,14 +166,21 @@ func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *
remarkParts := strings.Split(remark, "|")
if len(remarkParts) != 3 || remarkParts[0] != platform || remarkParts[1] != clientSessionId {
if len(remarkParts) != 4 || remarkParts[0] != platform || remarkParts[1] != clientSessionId {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid oauth 2.0 state \"%s\" in duplicate checker for client session id \"%s\"", remark, clientSessionId)
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2State)
}
verifier := remarkParts[2]
expectedState := fmt.Sprintf("%s|%s|%s", platform, clientSessionId, verifier)
expectedState = fmt.Sprintf("%s|%s|%s", platform, clientSessionId, utils.MD5EncodeToString([]byte(expectedState)))
uid, err := utils.StringToInt64(remarkParts[2])
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid uid \"%s\" in oauth 2.0 state \"%s\"", remarkParts[2], remark)
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2State)
}
verifier := remarkParts[3]
expectedRemark := fmt.Sprintf("%s|%s|%d|%s", platform, clientSessionId, uid, verifier)
expectedState := fmt.Sprintf("%s|%s|%s", platform, clientSessionId, utils.MD5EncodeToString([]byte(expectedRemark)))
if oauth2CallbackReq.State != expectedState {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] mismatched random string in oauth 2.0 state, expected \"%s\", got \"%s\"", expectedState, oauth2CallbackReq.State)
@@ -199,6 +229,11 @@ func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
if uid != 0 && userExternalAuth != nil && userExternalAuth.Uid != uid {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] oauth 2.0 external auth has been bound to another user \"uid:%d\", current user \"uid:%d\"", userExternalAuth.Uid, uid)
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2UserAlreadyBoundToAnotherUser)
}
var user *models.User
if err == nil { // user already bound to external auth, redirect to success page
@@ -209,17 +244,26 @@ func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
} else { // errors.Is(err, errs.ErrUserExternalAuthNotFound) // user not bound to external auth, try to bind or register new user
if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierEmail {
user, err = a.users.GetUserByEmail(c, oauth2UserInfo.Email)
} else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername {
user, err = a.users.GetUserByUsername(c, oauth2UserInfo.UserName)
} else {
user, err = a.users.GetUserByEmail(c, oauth2UserInfo.Email)
}
if uid != 0 {
user, err = a.users.GetUserById(c, uid)
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user by id %d, because %s", uid, err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
} else {
if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierEmail {
user, err = a.users.GetUserByEmail(c, oauth2UserInfo.Email)
} else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername {
user, err = a.users.GetUserByUsername(c, oauth2UserInfo.UserName)
} else {
user, err = a.users.GetUserByEmail(c, oauth2UserInfo.Email)
}
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
}
if user == nil && a.CurrentConfig().EnableUserRegister && a.CurrentConfig().OAuth2AutoRegister {
+11 -10
View File
@@ -6,14 +6,15 @@ import (
// Error codes related to oauth 2.0
var (
ErrOAuth2NotEnabled = NewNormalError(NormalSubcategoryOAuth2, 0, http.StatusBadRequest, "oauth2 not enabled")
ErrOAuth2AutoRegistrationNotEnabled = NewNormalError(NormalSubcategoryOAuth2, 1, http.StatusBadRequest, "oauth2 auto registration not enabled")
ErrInvalidOAuth2LoginRequest = NewNormalError(NormalSubcategoryOAuth2, 2, http.StatusBadRequest, "invalid oauth2 login request")
ErrInvalidOAuth2Callback = NewNormalError(NormalSubcategoryOAuth2, 3, http.StatusBadRequest, "invalid oauth2 callback")
ErrMissingOAuth2State = NewNormalError(NormalSubcategoryOAuth2, 4, http.StatusBadRequest, "missing state in oauth2 callback")
ErrMissingOAuth2Code = NewNormalError(NormalSubcategoryOAuth2, 5, http.StatusBadRequest, "missing code in oauth2 callback")
ErrInvalidOAuth2State = NewNormalError(NormalSubcategoryOAuth2, 6, http.StatusBadRequest, "invalid state in oauth2 callback")
ErrCannotRetrieveOAuth2Token = NewNormalError(NormalSubcategoryOAuth2, 7, http.StatusBadRequest, "cannot retrieve oauth2 token")
ErrInvalidOAuth2Token = NewNormalError(NormalSubcategoryOAuth2, 8, http.StatusBadRequest, "invalid oauth2 token")
ErrCannotRetrieveUserInfo = NewNormalError(NormalSubcategoryOAuth2, 9, http.StatusBadRequest, "cannot retrieve user info from oauth2 provider")
ErrOAuth2NotEnabled = NewNormalError(NormalSubcategoryOAuth2, 0, http.StatusBadRequest, "oauth2 not enabled")
ErrOAuth2AutoRegistrationNotEnabled = NewNormalError(NormalSubcategoryOAuth2, 1, http.StatusBadRequest, "oauth2 auto registration not enabled")
ErrInvalidOAuth2LoginRequest = NewNormalError(NormalSubcategoryOAuth2, 2, http.StatusBadRequest, "invalid oauth2 login request")
ErrInvalidOAuth2Callback = NewNormalError(NormalSubcategoryOAuth2, 3, http.StatusBadRequest, "invalid oauth2 callback")
ErrMissingOAuth2State = NewNormalError(NormalSubcategoryOAuth2, 4, http.StatusBadRequest, "missing state in oauth2 callback")
ErrMissingOAuth2Code = NewNormalError(NormalSubcategoryOAuth2, 5, http.StatusBadRequest, "missing code in oauth2 callback")
ErrInvalidOAuth2State = NewNormalError(NormalSubcategoryOAuth2, 6, http.StatusBadRequest, "invalid state in oauth2 callback")
ErrCannotRetrieveOAuth2Token = NewNormalError(NormalSubcategoryOAuth2, 7, http.StatusBadRequest, "cannot retrieve oauth2 token")
ErrInvalidOAuth2Token = NewNormalError(NormalSubcategoryOAuth2, 8, http.StatusBadRequest, "invalid oauth2 token")
ErrCannotRetrieveUserInfo = NewNormalError(NormalSubcategoryOAuth2, 9, http.StatusBadRequest, "cannot retrieve user info from oauth2 provider")
ErrOAuth2UserAlreadyBoundToAnotherUser = NewNormalError(NormalSubcategoryOAuth2, 10, http.StatusBadRequest, "oauth2 user already bound to another user")
)
+2
View File
@@ -4,6 +4,7 @@ package models
type OAuth2LoginRequest struct {
Platform string `form:"platform" binding:"required"`
ClientSessionId string `form:"client_session_id" binding:"required"`
Token string `form:"token"`
}
// OAuth2CallbackRequest represents all parameters of OAuth 2.0 callback request
@@ -18,4 +19,5 @@ type OAuth2CallbackRequest struct {
type OAuth2CallbackLoginRequest struct {
Password string `json:"password" binding:"omitempty,min=6,max=128"`
Passcode string `json:"passcode" binding:"omitempty,notBlank,len=6"`
Token string `json:"token" binding:"omitempty"`
}