diff --git a/controllers/token.go b/controllers/token.go index e770199a..ab139227 100644 --- a/controllers/token.go +++ b/controllers/token.go @@ -165,6 +165,8 @@ func (c *ApiController) GetOAuthCode() { // @Param client_secret query string true "OAuth client secret" // @Param code query string true "OAuth code" // @Success 200 {object} object.TokenWrapper The Response object +// @Success 400 {object} object.TokenError The Response object +// @Success 401 {object} object.TokenError The Response object // @router /login/oauth/access_token [post] func (c *ApiController) GetOAuthToken() { grantType := c.Input().Get("grant_type") @@ -200,6 +202,7 @@ func (c *ApiController) GetOAuthToken() { host := c.Ctx.Request.Host c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, tag, avatar) + c.SetTokenErrorHttpStatus() c.ServeJSON() } @@ -213,6 +216,8 @@ func (c *ApiController) GetOAuthToken() { // @Param client_id query string true "OAuth client id" // @Param client_secret query string false "OAuth client secret" // @Success 200 {object} object.TokenWrapper The Response object +// @Success 400 {object} object.TokenError The Response object +// @Success 401 {object} object.TokenError The Response object // @router /login/oauth/refresh_token [post] func (c *ApiController) RefreshToken() { grantType := c.Input().Get("grant_type") @@ -235,6 +240,7 @@ func (c *ApiController) RefreshToken() { } c.Data["json"] = object.RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host) + c.SetTokenErrorHttpStatus() c.ServeJSON() } @@ -270,6 +276,8 @@ func (c *ApiController) TokenLogout() { // @Param token formData string true "access_token's value or refresh_token's value" // @Param token_type_hint formData string true "the token type access_token or refresh_token" // @Success 200 {object} object.IntrospectionResponse The Response object +// @Success 400 {object} object.TokenError The Response object +// @Success 401 {object} object.TokenError The Response object // @router /login/oauth/introspect [post] func (c *ApiController) IntrospectToken() { tokenValue := c.Input().Get("token") @@ -279,12 +287,21 @@ func (c *ApiController) IntrospectToken() { clientSecret = c.Input().Get("client_secret") if clientId == "" || clientSecret == "" { c.ResponseError("empty clientId or clientSecret") + c.Data["json"] = &object.TokenError{ + Error: object.INVALID_REQUEST, + } + c.SetTokenErrorHttpStatus() + c.ServeJSON() return } } application := object.GetApplicationByClientId(clientId) if application == nil || application.ClientSecret != clientSecret { c.ResponseError("invalid application or wrong clientSecret") + c.Data["json"] = &object.TokenError{ + Error: object.INVALID_CLIENT, + } + c.SetTokenErrorHttpStatus() return } token := object.GetTokenByTokenAndApplication(tokenValue, application.Name) diff --git a/controllers/util.go b/controllers/util.go index 0b3b3c71..cfb545f4 100644 --- a/controllers/util.go +++ b/controllers/util.go @@ -51,6 +51,23 @@ func (c *ApiController) ResponseError(error string, data ...interface{}) { c.ServeJSON() } +// SetTokenErrorHttpStatus ... +func (c *ApiController) SetTokenErrorHttpStatus() { + _, ok := c.Data["json"].(*object.TokenError) + if ok { + if c.Data["json"].(*object.TokenError).Error == object.INVALID_CLIENT { + c.Ctx.Output.SetStatus(401) + c.Ctx.Output.Header("WWW-Authenticate", "Basic realm=\"OAuth2\"") + } else { + c.Ctx.Output.SetStatus(400) + } + } + _, ok = c.Data["json"].(*object.TokenWrapper) + if ok { + c.Ctx.Output.SetStatus(200) + } +} + // RequireSignedIn ... func (c *ApiController) RequireSignedIn() (string, bool) { userId := c.GetSessionUsername() diff --git a/object/token.go b/object/token.go index 836a5bcc..67044749 100644 --- a/object/token.go +++ b/object/token.go @@ -17,7 +17,6 @@ package object import ( "crypto/sha256" "encoding/base64" - "errors" "fmt" "strings" "time" @@ -28,7 +27,14 @@ import ( ) const ( - hourSeconds = 3600 + hourSeconds = 3600 + INVALID_REQUEST = "invalid_request" + INVALID_CLIENT = "invalid_client" + INVALID_GRANT = "invalid_grant" + UNAUTHORIZED_CLIENT = "unauthorized_client" + UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type" + INVALID_SCOPE = "invalid_scope" + ENDPOINT_ERROR = "endpoint_error" ) type Code struct { @@ -63,7 +69,11 @@ type TokenWrapper struct { TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` Scope string `json:"scope"` - Error string `json:"error,omitempty"` +} + +type TokenError struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description,omitempty"` } type IntrospectionResponse struct { @@ -311,59 +321,42 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU } } - -func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string, tag string, avatar string) *TokenWrapper { - var errString string +func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string, tag string, avatar string) interface{} { application := GetApplicationByClientId(clientId) if application == nil { - errString = "error: invalid client_id" - return &TokenWrapper{ - AccessToken: errString, - TokenType: "", - ExpiresIn: 0, - Scope: "", - Error: errString, + return &TokenError{ + Error: INVALID_CLIENT, + ErrorDescription: "client_id is invalid", } } //Check if grantType is allowed in the current application if !IsGrantTypeValid(grantType, application.GrantTypes) && tag == "" { - errString = fmt.Sprintf("error: grant_type: %s is not supported in this application", grantType) - return &TokenWrapper{ - AccessToken: errString, - TokenType: "", - ExpiresIn: 0, - Scope: "", - Error: errString, + return &TokenError{ + Error: UNSUPPORTED_GRANT_TYPE, + ErrorDescription: fmt.Sprintf("grant_type: %s is not supported in this application", grantType), } } var token *Token - var err error + var tokenError *TokenError switch grantType { case "authorization_code": // Authorization Code Grant - token, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier) + token, tokenError = GetAuthorizationCodeToken(application, clientSecret, code, verifier) case "password": // Resource Owner Password Credentials Grant - token, err = GetPasswordToken(application, username, password, scope, host) + token, tokenError = GetPasswordToken(application, username, password, scope, host) case "client_credentials": // Client Credentials Grant - token, err = GetClientCredentialsToken(application, clientSecret, scope, host) + token, tokenError = GetClientCredentialsToken(application, clientSecret, scope, host) } if tag == "wechat_miniprogram" { // Wechat Mini Program - token, err = GetWechatMiniProgramToken(application, code, host, username, avatar) + token, tokenError = GetWechatMiniProgramToken(application, code, host, username, avatar) } - if err != nil { - errString = err.Error() - return &TokenWrapper{ - AccessToken: errString, - TokenType: "", - ExpiresIn: 0, - Scope: "", - Error: errString, - } + if tokenError != nil { + return tokenError } token.CodeIsUsed = true @@ -380,81 +373,59 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code return tokenWrapper } -func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) *TokenWrapper { - var errString string +func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) interface{} { // check parameters if grantType != "refresh_token" { - errString = "error: grant_type should be \"refresh_token\"" - return &TokenWrapper{ - AccessToken: errString, - TokenType: "", - ExpiresIn: 0, - Scope: "", - Error: errString, + return &TokenError{ + Error: UNSUPPORTED_GRANT_TYPE, + ErrorDescription: "grant_type should be refresh_token", } } application := GetApplicationByClientId(clientId) if application == nil { - errString = "error: invalid client_id" - return &TokenWrapper{ - AccessToken: errString, - TokenType: "", - ExpiresIn: 0, - Scope: "", - Error: errString, + return &TokenError{ + Error: INVALID_CLIENT, + ErrorDescription: "client_id is invalid", } } if clientSecret != "" && application.ClientSecret != clientSecret { - errString = "error: invalid client_secret" - return &TokenWrapper{ - AccessToken: errString, - TokenType: "", - ExpiresIn: 0, - Scope: "", - Error: errString, + return &TokenError{ + Error: INVALID_CLIENT, + ErrorDescription: "client_secret is invalid", } } // check whether the refresh token is valid, and has not expired. token := Token{RefreshToken: refreshToken} existed, err := adapter.Engine.Get(&token) if err != nil || !existed { - errString = "error: invalid refresh_token" - return &TokenWrapper{ - AccessToken: errString, - TokenType: "", - ExpiresIn: 0, - Scope: "", - Error: errString, + return &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "refresh token is invalid, expired or revoked", } } cert := getCertByApplication(application) _, err = ParseJwtToken(refreshToken, cert) if err != nil { - errString := fmt.Sprintf("error: %s", err.Error()) - return &TokenWrapper{ - AccessToken: errString, - TokenType: "", - ExpiresIn: 0, - Scope: "", - Error: errString, + return &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()), } } // generate a new token user := getUser(application.Organization, token.User) if user.IsForbidden { - errString = "error: the user is forbidden to sign in, please contact the administrator" - return &TokenWrapper{ - AccessToken: errString, - TokenType: "", - ExpiresIn: 0, - Scope: "", - Error: errString, + return &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "the user is forbidden to sign in, please contact the administrator", } } newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "", scope, host) if err != nil { - panic(err) + return &TokenError{ + Error: ENDPOINT_ERROR, + ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()), + } } newToken := &Token{ @@ -508,63 +479,99 @@ func IsGrantTypeValid(method string, grantTypes []string) bool { } // Authorization code flow -func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, error) { +func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, *TokenError) { if code == "" { - return nil, errors.New("error: authorization code should not be empty") + return nil, &TokenError{ + Error: INVALID_REQUEST, + ErrorDescription: "authorization code should not be empty", + } } token := getTokenByCode(code) if token == nil { - return nil, errors.New("error: invalid authorization code") + return nil, &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "authorization code is invalid", + } } if token.CodeIsUsed { // anti replay attacks - return nil, errors.New("error: authorization code has been used") + return nil, &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "authorization code has been used", + } } if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge { - return nil, errors.New("error: incorrect code_verifier") + return nil, &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "verifier is invalid", + } } if application.ClientSecret != clientSecret { // when using PKCE, the Client Secret can be empty, // but if it is provided, it must be accurate. if token.CodeChallenge == "" { - return nil, errors.New("error: invalid client_secret") + return nil, &TokenError{ + Error: INVALID_CLIENT, + ErrorDescription: "client_secret is invalid", + } } else { if clientSecret != "" { - return nil, errors.New("error: invalid client_secret") + return nil, &TokenError{ + Error: INVALID_CLIENT, + ErrorDescription: "client_secret is invalid", + } } } } if application.Name != token.Application { - return nil, errors.New("error: the token is for wrong application (client_id)") + return nil, &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "the token is for wrong application (client_id)", + } } if time.Now().Unix() > token.CodeExpireIn { // code must be used within 5 minutes - return nil, errors.New("error: authorization code has expired") + return nil, &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "authorization code has expired", + } } return token, nil } // Resource Owner Password Credentials flow -func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, error) { +func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, *TokenError) { user := getUser(application.Organization, username) if user == nil { - return nil, errors.New("error: the user does not exist") + return nil, &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "the user does not exist", + } } msg := CheckPassword(user, password) if msg != "" { - return nil, errors.New("error: invalid username or password") + return nil, &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "invalid username or password", + } } if user.IsForbidden { - return nil, errors.New("error: the user is forbidden to sign in, please contact the administrator") + return nil, &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "the user is forbidden to sign in, please contact the administrator", + } } accessToken, refreshToken, err := generateJwtToken(application, user, "", scope, host) if err != nil { - return nil, err + return nil, &TokenError{ + Error: ENDPOINT_ERROR, + ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()), + } } token := &Token{ Owner: application.Owner, @@ -586,9 +593,12 @@ func GetPasswordToken(application *Application, username string, password string } // Client Credentials flow -func GetClientCredentialsToken(application *Application, clientSecret string, scope string, host string) (*Token, error) { +func GetClientCredentialsToken(application *Application, clientSecret string, scope string, host string) (*Token, *TokenError) { if application.ClientSecret != clientSecret { - return nil, errors.New("error: invalid client_secret") + return nil, &TokenError{ + Error: INVALID_CLIENT, + ErrorDescription: "client_secret is invalid", + } } nullUser := &User{ Owner: application.Owner, @@ -597,7 +607,10 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc } accessToken, _, err := generateJwtToken(application, nullUser, "", scope, host) if err != nil { - return nil, err + return nil, &TokenError{ + Error: ENDPOINT_ERROR, + ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()), + } } token := &Token{ Owner: application.Owner, @@ -643,25 +656,37 @@ func GetTokenByUser(application *Application, user *User, scope string, host str } // Wechat Mini Program flow -func GetWechatMiniProgramToken(application *Application, code string, host string, username string, avatar string) (*Token, error) { +func GetWechatMiniProgramToken(application *Application, code string, host string, username string, avatar string) (*Token, *TokenError) { mpProvider := GetWechatMiniProgramProvider(application) if mpProvider == nil { - return nil, errors.New("error: the application does not support wechat mini program") + return nil, &TokenError{ + Error: INVALID_CLIENT, + ErrorDescription: "the application does not support wechat mini program", + } } provider := GetProvider(util.GetId(mpProvider.Name)) mpIdp := idp.NewWeChatMiniProgramIdProvider(provider.ClientId, provider.ClientSecret) session, err := mpIdp.GetSessionByCode(code) if err != nil { - return nil, err + return nil, &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: fmt.Sprintf("get wechat mini program session error: %s", err.Error()), + } } openId, unionId := session.Openid, session.Unionid if openId == "" && unionId == "" { - return nil, errors.New("err: WeChat's openid and unionid are empty") + return nil, &TokenError{ + Error: INVALID_REQUEST, + ErrorDescription: "the wechat mini program session is invalid", + } } user := getUserByWechatId(openId, unionId) if user == nil { if !application.EnableSignUp { - return nil, errors.New("err: the application does not allow to sign up new account") + return nil, &TokenError{ + Error: INVALID_GRANT, + ErrorDescription: "the application does not allow to sign up new account", + } } //Add new user var name string @@ -691,7 +716,10 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin accessToken, refreshToken, err := generateJwtToken(application, user, "", "", host) if err != nil { - return nil, err + return nil, &TokenError{ + Error: ENDPOINT_ERROR, + ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()), + } } token := &Token{ diff --git a/swagger/swagger.json b/swagger/swagger.json index 1f932ff0..21a42ed9 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -2194,6 +2194,18 @@ "schema": { "$ref": "#/definitions/object.TokenWrapper" } + }, + "400": { + "description": "The Response object", + "schema": { + "$ref": "#/definitions/object.TokenError" + } + }, + "401": { + "description": "The Response object", + "schema": { + "$ref": "#/definitions/object.TokenError" + } } } } @@ -2285,6 +2297,18 @@ "schema": { "$ref": "#/definitions/object.IntrospectionResponse" } + }, + "400": { + "description": "The Response object", + "schema": { + "$ref": "#/definitions/object.TokenError" + } + }, + "401": { + "description": "The Response object", + "schema": { + "$ref": "#/definitions/object.TokenError" + } } } } @@ -2377,6 +2401,18 @@ "schema": { "$ref": "#/definitions/object.TokenWrapper" } + }, + "400": { + "description": "The Response object", + "schema": { + "$ref": "#/definitions/object.TokenError" + } + }, + "401": { + "description": "The Response object", + "schema": { + "$ref": "#/definitions/object.TokenError" + } } } } @@ -3063,11 +3099,11 @@ } }, "definitions": { - "2200.0xc0003c4b70.false": { + "2127.0xc000398090.false": { "title": "false", "type": "object" }, - "2235.0xc0003c4ba0.false": { + "2161.0xc0003980c0.false": { "title": "false", "type": "object" }, @@ -3082,6 +3118,9 @@ "content": { "type": "string" }, + "provider": { + "type": "string" + }, "receivers": { "type": "array", "items": { @@ -3182,10 +3221,10 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/2200.0xc0003c4b70.false" + "$ref": "#/definitions/2127.0xc000398090.false" }, "data2": { - "$ref": "#/definitions/2235.0xc0003c4ba0.false" + "$ref": "#/definitions/2161.0xc0003980c0.false" }, "msg": { "type": "string" @@ -4209,6 +4248,18 @@ } } }, + "object.TokenError": { + "title": "TokenError", + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "error_description": { + "type": "string" + } + } + }, "object.TokenWrapper": { "title": "TokenWrapper", "type": "object", @@ -4216,9 +4267,6 @@ "access_token": { "type": "string" }, - "error": { - "type": "string" - }, "expires_in": { "type": "integer", "format": "int64" diff --git a/swagger/swagger.yml b/swagger/swagger.yml index 93c426bc..2ad6d21e 100644 --- a/swagger/swagger.yml +++ b/swagger/swagger.yml @@ -1435,6 +1435,14 @@ paths: description: The Response object schema: $ref: '#/definitions/object.TokenWrapper' + "400": + description: The Response object + schema: + $ref: '#/definitions/object.TokenError' + "401": + description: The Response object + schema: + $ref: '#/definitions/object.TokenError' /api/login/oauth/code: post: tags: @@ -1497,6 +1505,14 @@ paths: description: The Response object schema: $ref: '#/definitions/object.IntrospectionResponse' + "400": + description: The Response object + schema: + $ref: '#/definitions/object.TokenError' + "401": + description: The Response object + schema: + $ref: '#/definitions/object.TokenError' /api/login/oauth/logout: get: tags: @@ -1559,6 +1575,14 @@ paths: description: The Response object schema: $ref: '#/definitions/object.TokenWrapper' + "400": + description: The Response object + schema: + $ref: '#/definitions/object.TokenError' + "401": + description: The Response object + schema: + $ref: '#/definitions/object.TokenError' /api/logout: post: tags: @@ -2005,10 +2029,10 @@ paths: - Verification API operationId: ApiController.VerifyCaptcha definitions: - 2200.0xc0003c4b70.false: + 2127.0xc000398090.false: title: "false" type: object - 2235.0xc0003c4ba0.false: + 2161.0xc0003980c0.false: title: "false" type: object Response: @@ -2020,6 +2044,8 @@ definitions: properties: content: type: string + provider: + type: string receivers: type: array items: @@ -2087,9 +2113,9 @@ definitions: type: object properties: data: - $ref: '#/definitions/2200.0xc0003c4b70.false' + $ref: '#/definitions/2127.0xc000398090.false' data2: - $ref: '#/definitions/2235.0xc0003c4ba0.false' + $ref: '#/definitions/2161.0xc0003980c0.false' msg: type: string name: @@ -2776,14 +2802,20 @@ definitions: type: string user: type: string + object.TokenError: + title: TokenError + type: object + properties: + error: + type: string + error_description: + type: string object.TokenWrapper: title: TokenWrapper type: object properties: access_token: type: string - error: - type: string expires_in: type: integer format: int64