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>
This commit is contained in:
parent
6a3ad0b24e
commit
45970ae82e
3 changed files with 75 additions and 0 deletions
|
@ -410,6 +410,7 @@ func RegisterRoutes(m *web.Route) {
|
||||||
// TODO manage redirection
|
// TODO manage redirection
|
||||||
m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
|
m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
|
||||||
}, ignSignInAndCsrf, reqSignIn)
|
}, ignSignInAndCsrf, reqSignIn)
|
||||||
|
m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
|
||||||
if setting.CORSConfig.Enabled {
|
if setting.CORSConfig.Enabled {
|
||||||
m.Post("/login/oauth/access_token", cors.Handler(cors.Options{
|
m.Post("/login/oauth/access_token", cors.Handler(cors.Options{
|
||||||
//Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option
|
//Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/auth/sso"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -93,6 +94,24 @@ func (err AccessTokenError) Error() string {
|
||||||
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
|
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
|
// TokenType specifies the kind of token
|
||||||
type TokenType string
|
type TokenType string
|
||||||
|
|
||||||
|
@ -193,6 +212,45 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*Ac
|
||||||
}, nil
|
}, 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
|
// AuthorizeOAuth manages authorize requests
|
||||||
func AuthorizeOAuth(ctx *context.Context) {
|
func AuthorizeOAuth(ctx *context.Context) {
|
||||||
form := web.GetForm(ctx).(*forms.AuthorizationForm)
|
form := web.GetForm(ctx).(*forms.AuthorizationForm)
|
||||||
|
@ -571,3 +629,18 @@ func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirect
|
||||||
redirect.RawQuery = q.Encode()
|
redirect.RawQuery = q.Encode()
|
||||||
ctx.Redirect(redirect.String(), 302)
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"issuer": "{{AppUrl | JSEscape | Safe}}",
|
"issuer": "{{AppUrl | JSEscape | Safe}}",
|
||||||
"authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize",
|
"authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize",
|
||||||
"token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
|
"token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
|
||||||
|
"userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
|
||||||
"response_types_supported": [
|
"response_types_supported": [
|
||||||
"code",
|
"code",
|
||||||
"id_token"
|
"id_token"
|
||||||
|
|
Loading…
Reference in a new issue