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)
|
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
|
||||||
|
|
||||||
|
@ -253,35 +235,56 @@ type userInfoResponse struct {
|
||||||
|
|
||||||
// InfoOAuth manages request for userinfo endpoint
|
// InfoOAuth manages request for userinfo endpoint
|
||||||
func InfoOAuth(ctx *context.Context) {
|
func InfoOAuth(ctx *context.Context) {
|
||||||
header := ctx.Req.Header.Get("Authorization")
|
if ctx.User == nil || ctx.Data["AuthedMethod"] != (&auth.OAuth2{}).Name() {
|
||||||
auths := strings.Fields(header)
|
ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
|
||||||
if len(auths) != 2 || auths[0] != "Bearer" {
|
ctx.HandleText(http.StatusUnauthorized, "no valid authorization")
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
response := &userInfoResponse{
|
response := &userInfoResponse{
|
||||||
Sub: fmt.Sprint(authUser.ID),
|
Sub: fmt.Sprint(ctx.User.ID),
|
||||||
Name: authUser.FullName,
|
Name: ctx.User.FullName,
|
||||||
Username: authUser.Name,
|
Username: ctx.User.Name,
|
||||||
Email: authUser.Email,
|
Email: ctx.User.Email,
|
||||||
Picture: authUser.AvatarLink(),
|
Picture: ctx.User.AvatarLink(),
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, response)
|
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
|
// 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)
|
||||||
|
@ -697,18 +700,3 @@ 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -311,6 +311,7 @@ func RegisterRoutes(m *web.Route) {
|
||||||
m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
|
m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
|
||||||
m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
|
m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
|
||||||
m.Get("/login/oauth/keys", ignSignInAndCsrf, user.OIDCKeys)
|
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.Group("/user/settings", func() {
|
||||||
m.Get("", userSetting.Profile)
|
m.Get("", userSetting.Profile)
|
||||||
|
|
|
@ -113,7 +113,7 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) {
|
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) {
|
||||||
return nil
|
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)
|
log.Trace("OAuth2 Authorization: Logged in user %-v", user)
|
||||||
return 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)
|
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",
|
"token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
|
||||||
"jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys",
|
"jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys",
|
||||||
"userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
|
"userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
|
||||||
|
"introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect",
|
||||||
"response_types_supported": [
|
"response_types_supported": [
|
||||||
"code",
|
"code",
|
||||||
"id_token"
|
"id_token"
|
||||||
|
|
Reference in a new issue