From 45970ae82e478dd7d5f01fcc053de5df00198abc Mon Sep 17 00:00:00 2001 From: "N. L. H" Date: Thu, 6 May 2021 07:30:15 +0200 Subject: [PATCH] Feature/oauth userinfo (#15721) * Implemented userinfo #8534 * Make lint happy * Add userinfo endpoint to openid-configuration * Give an error when uid equals 0 * Implemented BearerTokenErrorCode handling * instead of ctx.error use ctx.json so that clients parse error and error_description correctly * Removed unneeded if statement * Use switch instead of subsequent if statements Have a default for unknown errorcodes. Co-authored-by: Nils Hillmann Co-authored-by: nlhsoftware --- routers/routes/web.go | 1 + routers/user/oauth.go | 73 +++++++++++++++++++++++++ templates/user/auth/oidc_wellknown.tmpl | 1 + 3 files changed, 75 insertions(+) diff --git a/routers/routes/web.go b/routers/routes/web.go index ebd738de1..c4d0bc32f 100644 --- a/routers/routes/web.go +++ b/routers/routes/web.go @@ -410,6 +410,7 @@ func RegisterRoutes(m *web.Route) { // TODO manage redirection m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth) }, ignSignInAndCsrf, reqSignIn) + m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth) if setting.CORSConfig.Enabled { m.Post("/login/oauth/access_token", cors.Handler(cors.Options{ //Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option diff --git a/routers/user/oauth.go b/routers/user/oauth.go index ae06efd0c..3ef5a56c0 100644 --- a/routers/user/oauth.go +++ b/routers/user/oauth.go @@ -13,6 +13,7 @@ import ( "strings" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth/sso" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" @@ -93,6 +94,24 @@ func (err AccessTokenError) Error() string { return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription) } +// BearerTokenErrorCode represents an error code specified in RFC 6750 +type BearerTokenErrorCode string + +const ( + // BearerTokenErrorCodeInvalidRequest represents an error code specified in RFC 6750 + BearerTokenErrorCodeInvalidRequest BearerTokenErrorCode = "invalid_request" + // BearerTokenErrorCodeInvalidToken represents an error code specified in RFC 6750 + BearerTokenErrorCodeInvalidToken BearerTokenErrorCode = "invalid_token" + // BearerTokenErrorCodeInsufficientScope represents an error code specified in RFC 6750 + BearerTokenErrorCodeInsufficientScope BearerTokenErrorCode = "insufficient_scope" +) + +// BearerTokenError represents an error response specified in RFC 6750 +type BearerTokenError struct { + ErrorCode BearerTokenErrorCode `json:"error" form:"error"` + ErrorDescription string `json:"error_description"` +} + // TokenType specifies the kind of token type TokenType string @@ -193,6 +212,45 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*Ac }, nil } +type userInfoResponse struct { + Sub string `json:"sub"` + Name string `json:"name"` + Username string `json:"preferred_username"` + Email string `json:"email"` + Picture string `json:"picture"` +} + +// InfoOAuth manages request for userinfo endpoint +func InfoOAuth(ctx *context.Context) { + header := ctx.Req.Header.Get("Authorization") + auths := strings.Fields(header) + if len(auths) != 2 || auths[0] != "Bearer" { + ctx.HandleText(http.StatusUnauthorized, "no valid auth token authorization") + return + } + uid := sso.CheckOAuthAccessToken(auths[1]) + if uid == 0 { + handleBearerTokenError(ctx, BearerTokenError{ + ErrorCode: BearerTokenErrorCodeInvalidToken, + ErrorDescription: "Access token not assigned to any user", + }) + return + } + authUser, err := models.GetUserByID(uid) + if err != nil { + ctx.ServerError("GetUserByID", err) + return + } + response := &userInfoResponse{ + Sub: fmt.Sprint(authUser.ID), + Name: authUser.FullName, + Username: authUser.Name, + Email: authUser.Email, + Picture: authUser.AvatarLink(), + } + ctx.JSON(http.StatusOK, response) +} + // AuthorizeOAuth manages authorize requests func AuthorizeOAuth(ctx *context.Context) { form := web.GetForm(ctx).(*forms.AuthorizationForm) @@ -571,3 +629,18 @@ func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirect redirect.RawQuery = q.Encode() ctx.Redirect(redirect.String(), 302) } + +func handleBearerTokenError(ctx *context.Context, beErr BearerTokenError) { + ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"\", error=\"%s\", error_description=\"%s\"", beErr.ErrorCode, beErr.ErrorDescription)) + switch beErr.ErrorCode { + case BearerTokenErrorCodeInvalidRequest: + ctx.JSON(http.StatusBadRequest, beErr) + case BearerTokenErrorCodeInvalidToken: + ctx.JSON(http.StatusUnauthorized, beErr) + case BearerTokenErrorCodeInsufficientScope: + ctx.JSON(http.StatusForbidden, beErr) + default: + log.Error("Invalid BearerTokenErrorCode: %v", beErr.ErrorCode) + ctx.ServerError("Unhandled BearerTokenError", fmt.Errorf("BearerTokenError: error=\"%v\", error_description=\"%v\"", beErr.ErrorCode, beErr.ErrorDescription)) + } +} diff --git a/templates/user/auth/oidc_wellknown.tmpl b/templates/user/auth/oidc_wellknown.tmpl index 290ed4a71..fcde060a8 100644 --- a/templates/user/auth/oidc_wellknown.tmpl +++ b/templates/user/auth/oidc_wellknown.tmpl @@ -2,6 +2,7 @@ "issuer": "{{AppUrl | JSEscape | Safe}}", "authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize", "token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token", + "userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo", "response_types_supported": [ "code", "id_token"