Skip to content

Commit 45970ae

Browse files
NLH-SoftwareNils Hillmannnlhsoftware
authored
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 <hillmann@nlh-software.de> Co-authored-by: nlhsoftware <nlhsoftware@noreply.localhost>
1 parent 6a3ad0b commit 45970ae

File tree

3 files changed

+75
-0
lines changed

3 files changed

+75
-0
lines changed

routers/routes/web.go

+1
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ func RegisterRoutes(m *web.Route) {
410410
// TODO manage redirection
411411
m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
412412
}, ignSignInAndCsrf, reqSignIn)
413+
m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
413414
if setting.CORSConfig.Enabled {
414415
m.Post("/login/oauth/access_token", cors.Handler(cors.Options{
415416
//Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option

routers/user/oauth.go

+73
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strings"
1414

1515
"code.gitea.io/gitea/models"
16+
"code.gitea.io/gitea/modules/auth/sso"
1617
"code.gitea.io/gitea/modules/base"
1718
"code.gitea.io/gitea/modules/context"
1819
"code.gitea.io/gitea/modules/log"
@@ -93,6 +94,24 @@ func (err AccessTokenError) Error() string {
9394
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
9495
}
9596

97+
// BearerTokenErrorCode represents an error code specified in RFC 6750
98+
type BearerTokenErrorCode string
99+
100+
const (
101+
// BearerTokenErrorCodeInvalidRequest represents an error code specified in RFC 6750
102+
BearerTokenErrorCodeInvalidRequest BearerTokenErrorCode = "invalid_request"
103+
// BearerTokenErrorCodeInvalidToken represents an error code specified in RFC 6750
104+
BearerTokenErrorCodeInvalidToken BearerTokenErrorCode = "invalid_token"
105+
// BearerTokenErrorCodeInsufficientScope represents an error code specified in RFC 6750
106+
BearerTokenErrorCodeInsufficientScope BearerTokenErrorCode = "insufficient_scope"
107+
)
108+
109+
// BearerTokenError represents an error response specified in RFC 6750
110+
type BearerTokenError struct {
111+
ErrorCode BearerTokenErrorCode `json:"error" form:"error"`
112+
ErrorDescription string `json:"error_description"`
113+
}
114+
96115
// TokenType specifies the kind of token
97116
type TokenType string
98117

@@ -193,6 +212,45 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*Ac
193212
}, nil
194213
}
195214

215+
type userInfoResponse struct {
216+
Sub string `json:"sub"`
217+
Name string `json:"name"`
218+
Username string `json:"preferred_username"`
219+
Email string `json:"email"`
220+
Picture string `json:"picture"`
221+
}
222+
223+
// InfoOAuth manages request for userinfo endpoint
224+
func InfoOAuth(ctx *context.Context) {
225+
header := ctx.Req.Header.Get("Authorization")
226+
auths := strings.Fields(header)
227+
if len(auths) != 2 || auths[0] != "Bearer" {
228+
ctx.HandleText(http.StatusUnauthorized, "no valid auth token authorization")
229+
return
230+
}
231+
uid := sso.CheckOAuthAccessToken(auths[1])
232+
if uid == 0 {
233+
handleBearerTokenError(ctx, BearerTokenError{
234+
ErrorCode: BearerTokenErrorCodeInvalidToken,
235+
ErrorDescription: "Access token not assigned to any user",
236+
})
237+
return
238+
}
239+
authUser, err := models.GetUserByID(uid)
240+
if err != nil {
241+
ctx.ServerError("GetUserByID", err)
242+
return
243+
}
244+
response := &userInfoResponse{
245+
Sub: fmt.Sprint(authUser.ID),
246+
Name: authUser.FullName,
247+
Username: authUser.Name,
248+
Email: authUser.Email,
249+
Picture: authUser.AvatarLink(),
250+
}
251+
ctx.JSON(http.StatusOK, response)
252+
}
253+
196254
// AuthorizeOAuth manages authorize requests
197255
func AuthorizeOAuth(ctx *context.Context) {
198256
form := web.GetForm(ctx).(*forms.AuthorizationForm)
@@ -571,3 +629,18 @@ func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirect
571629
redirect.RawQuery = q.Encode()
572630
ctx.Redirect(redirect.String(), 302)
573631
}
632+
633+
func handleBearerTokenError(ctx *context.Context, beErr BearerTokenError) {
634+
ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"\", error=\"%s\", error_description=\"%s\"", beErr.ErrorCode, beErr.ErrorDescription))
635+
switch beErr.ErrorCode {
636+
case BearerTokenErrorCodeInvalidRequest:
637+
ctx.JSON(http.StatusBadRequest, beErr)
638+
case BearerTokenErrorCodeInvalidToken:
639+
ctx.JSON(http.StatusUnauthorized, beErr)
640+
case BearerTokenErrorCodeInsufficientScope:
641+
ctx.JSON(http.StatusForbidden, beErr)
642+
default:
643+
log.Error("Invalid BearerTokenErrorCode: %v", beErr.ErrorCode)
644+
ctx.ServerError("Unhandled BearerTokenError", fmt.Errorf("BearerTokenError: error=\"%v\", error_description=\"%v\"", beErr.ErrorCode, beErr.ErrorDescription))
645+
}
646+
}

templates/user/auth/oidc_wellknown.tmpl

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"issuer": "{{AppUrl | JSEscape | Safe}}",
33
"authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize",
44
"token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
5+
"userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
56
"response_types_supported": [
67
"code",
78
"id_token"

0 commit comments

Comments
 (0)