Added introspection endpoint. (#16752)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
parent
e9747de952
commit
0bd58d61e5
5 changed files with 67 additions and 56 deletions
|
@ -96,24 +96,6 @@ 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
|
||||
|
||||
|
@ -253,35 +235,56 @@ type userInfoResponse struct {
|
|||
|
||||
// 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 := auth.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)
|
||||
if ctx.User == nil || ctx.Data["AuthedMethod"] != (&auth.OAuth2{}).Name() {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
|
||||
ctx.HandleText(http.StatusUnauthorized, "no valid authorization")
|
||||
return
|
||||
}
|
||||
response := &userInfoResponse{
|
||||
Sub: fmt.Sprint(authUser.ID),
|
||||
Name: authUser.FullName,
|
||||
Username: authUser.Name,
|
||||
Email: authUser.Email,
|
||||
Picture: authUser.AvatarLink(),
|
||||
Sub: fmt.Sprint(ctx.User.ID),
|
||||
Name: ctx.User.FullName,
|
||||
Username: ctx.User.Name,
|
||||
Email: ctx.User.Email,
|
||||
Picture: ctx.User.AvatarLink(),
|
||||
}
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// IntrospectOAuth introspects an oauth token
|
||||
func IntrospectOAuth(ctx *context.Context) {
|
||||
if ctx.User == nil {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
|
||||
ctx.HandleText(http.StatusUnauthorized, "no valid authorization")
|
||||
return
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Active bool `json:"active"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.IntrospectTokenForm)
|
||||
token, err := oauth2.ParseToken(form.Token)
|
||||
if err == nil {
|
||||
if token.Valid() == nil {
|
||||
grant, err := models.GetOAuth2GrantByID(token.GrantID)
|
||||
if err == nil && grant != nil {
|
||||
app, err := models.GetOAuth2ApplicationByID(grant.ApplicationID)
|
||||
if err == nil && app != nil {
|
||||
response.Active = true
|
||||
response.Scope = grant.Scope
|
||||
response.Issuer = setting.AppURL
|
||||
response.Audience = app.ClientID
|
||||
response.Subject = fmt.Sprint(grant.UserID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// AuthorizeOAuth manages authorize requests
|
||||
func AuthorizeOAuth(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.AuthorizationForm)
|
||||
|
@ -697,18 +700,3 @@ 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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -311,6 +311,7 @@ func RegisterRoutes(m *web.Route) {
|
|||
m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
|
||||
m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
|
||||
m.Get("/login/oauth/keys", ignSignInAndCsrf, user.OIDCKeys)
|
||||
m.Post("/login/oauth/introspect", CorsHandler(), bindIgnErr(forms.IntrospectTokenForm{}), ignSignInAndCsrf, user.IntrospectOAuth)
|
||||
|
||||
m.Group("/user/settings", func() {
|
||||
m.Get("", userSetting.Profile)
|
||||
|
|
|
@ -113,7 +113,7 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
|
|||
return nil
|
||||
}
|
||||
|
||||
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) {
|
||||
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -134,3 +134,13 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
|
|||
log.Trace("OAuth2 Authorization: Logged in user %-v", user)
|
||||
return user
|
||||
}
|
||||
|
||||
func isAuthenticatedTokenRequest(req *http.Request) bool {
|
||||
switch req.URL.Path {
|
||||
case "/login/oauth/userinfo":
|
||||
fallthrough
|
||||
case "/login/oauth/introspect":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -215,6 +215,17 @@ func (f *AccessTokenForm) Validate(req *http.Request, errs binding.Errors) bindi
|
|||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// IntrospectTokenForm for introspecting tokens
|
||||
type IntrospectTokenForm struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *IntrospectTokenForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// __________________________________________.___ _______ ________ _________
|
||||
// / _____/\_ _____/\__ ___/\__ ___/| |\ \ / _____/ / _____/
|
||||
// \_____ \ | __)_ | | | | | |/ | \/ \ ___ \_____ \
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
|
||||
"jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys",
|
||||
"userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
|
||||
"introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect",
|
||||
"response_types_supported": [
|
||||
"code",
|
||||
"id_token"
|
||||
|
|
Reference in a new issue