]> source.dussan.org Git - gitea.git/commitdiff
Feature/oauth userinfo (#15721)
authorN. L. H <github@nlh-software.de>
Thu, 6 May 2021 05:30:15 +0000 (07:30 +0200)
committerGitHub <noreply@github.com>
Thu, 6 May 2021 05:30:15 +0000 (01:30 -0400)
* 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 <hillmann@nlh-software.de>
Co-authored-by: nlhsoftware <nlhsoftware@noreply.localhost>
routers/routes/web.go
routers/user/oauth.go
templates/user/auth/oidc_wellknown.tmpl

index ebd738de1eab7d8c4aebd2b48d9f2ed960107100..c4d0bc32f87c7962e0939c88d387bc504dfab5de 100644 (file)
@@ -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
index ae06efd0c0168b55886b9520ec2e90713b6b7b63..3ef5a56c01734e9b095e2f14c43a1960ab001e7e 100644 (file)
@@ -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))
+       }
+}
index 290ed4a71df401448b196186fc23452e6bb4a8cd..fcde060a8d19f23261f7baab61de3f5117a9a3dc 100644 (file)
@@ -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"