forked from Shiloh/githaven
bbffcc3aec
There are multiple places where Gitea does not properly escape URLs that it is building and there are multiple places where it builds urls when there is already a simpler function available to use this. This is an extensive PR attempting to fix these issues. 1. The first commit in this PR looks through all href, src and links in the Gitea codebase and has attempted to catch all the places where there is potentially incomplete escaping. 2. Whilst doing this we will prefer to use functions that create URLs over recreating them by hand. 3. All uses of strings should be directly escaped - even if they are not currently expected to contain escaping characters. The main benefit to doing this will be that we can consider relaxing the constraints on user names and reponames in future. 4. The next commit looks at escaping in the wiki and re-considers the urls that are used there. Using the improved escaping here wiki files containing '/'. (This implementation will currently still place all of the wiki files the root directory of the repo but this would not be difficult to change.) 5. The title generation in feeds is now properly escaped. 6. EscapePound is no longer needed - urls should be PathEscaped / QueryEscaped as necessary but then re-escaped with Escape when creating html with locales Signed-off-by: Andrew Thornton <art27@cantab.net> Signed-off-by: Andrew Thornton <art27@cantab.net>
752 lines
24 KiB
Go
752 lines
24 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package user
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"html"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models"
|
|
"code.gitea.io/gitea/models/login"
|
|
"code.gitea.io/gitea/modules/base"
|
|
"code.gitea.io/gitea/modules/context"
|
|
"code.gitea.io/gitea/modules/json"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/modules/web"
|
|
"code.gitea.io/gitea/services/auth"
|
|
"code.gitea.io/gitea/services/auth/source/oauth2"
|
|
"code.gitea.io/gitea/services/forms"
|
|
|
|
"gitea.com/go-chi/binding"
|
|
"github.com/golang-jwt/jwt"
|
|
)
|
|
|
|
const (
|
|
tplGrantAccess base.TplName = "user/auth/grant"
|
|
tplGrantError base.TplName = "user/auth/grant_error"
|
|
)
|
|
|
|
// TODO move error and responses to SDK or models
|
|
|
|
// AuthorizeErrorCode represents an error code specified in RFC 6749
|
|
type AuthorizeErrorCode string
|
|
|
|
const (
|
|
// ErrorCodeInvalidRequest represents the according error in RFC 6749
|
|
ErrorCodeInvalidRequest AuthorizeErrorCode = "invalid_request"
|
|
// ErrorCodeUnauthorizedClient represents the according error in RFC 6749
|
|
ErrorCodeUnauthorizedClient AuthorizeErrorCode = "unauthorized_client"
|
|
// ErrorCodeAccessDenied represents the according error in RFC 6749
|
|
ErrorCodeAccessDenied AuthorizeErrorCode = "access_denied"
|
|
// ErrorCodeUnsupportedResponseType represents the according error in RFC 6749
|
|
ErrorCodeUnsupportedResponseType AuthorizeErrorCode = "unsupported_response_type"
|
|
// ErrorCodeInvalidScope represents the according error in RFC 6749
|
|
ErrorCodeInvalidScope AuthorizeErrorCode = "invalid_scope"
|
|
// ErrorCodeServerError represents the according error in RFC 6749
|
|
ErrorCodeServerError AuthorizeErrorCode = "server_error"
|
|
// ErrorCodeTemporaryUnavailable represents the according error in RFC 6749
|
|
ErrorCodeTemporaryUnavailable AuthorizeErrorCode = "temporarily_unavailable"
|
|
)
|
|
|
|
// AuthorizeError represents an error type specified in RFC 6749
|
|
type AuthorizeError struct {
|
|
ErrorCode AuthorizeErrorCode `json:"error" form:"error"`
|
|
ErrorDescription string
|
|
State string
|
|
}
|
|
|
|
// Error returns the error message
|
|
func (err AuthorizeError) Error() string {
|
|
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
|
|
}
|
|
|
|
// AccessTokenErrorCode represents an error code specified in RFC 6749
|
|
type AccessTokenErrorCode string
|
|
|
|
const (
|
|
// AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749
|
|
AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request"
|
|
// AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749
|
|
AccessTokenErrorCodeInvalidClient = "invalid_client"
|
|
// AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749
|
|
AccessTokenErrorCodeInvalidGrant = "invalid_grant"
|
|
// AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749
|
|
AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client"
|
|
// AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749
|
|
AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type"
|
|
// AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749
|
|
AccessTokenErrorCodeInvalidScope = "invalid_scope"
|
|
)
|
|
|
|
// AccessTokenError represents an error response specified in RFC 6749
|
|
type AccessTokenError struct {
|
|
ErrorCode AccessTokenErrorCode `json:"error" form:"error"`
|
|
ErrorDescription string `json:"error_description"`
|
|
}
|
|
|
|
// Error returns the error message
|
|
func (err AccessTokenError) Error() string {
|
|
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
|
|
}
|
|
|
|
// TokenType specifies the kind of token
|
|
type TokenType string
|
|
|
|
const (
|
|
// TokenTypeBearer represents a token type specified in RFC 6749
|
|
TokenTypeBearer TokenType = "bearer"
|
|
// TokenTypeMAC represents a token type specified in RFC 6749
|
|
TokenTypeMAC = "mac"
|
|
)
|
|
|
|
// AccessTokenResponse represents a successful access token response
|
|
type AccessTokenResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
TokenType TokenType `json:"token_type"`
|
|
ExpiresIn int64 `json:"expires_in"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
IDToken string `json:"id_token,omitempty"`
|
|
}
|
|
|
|
func newAccessTokenResponse(grant *login.OAuth2Grant, serverKey, clientKey oauth2.JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
|
|
if setting.OAuth2.InvalidateRefreshTokens {
|
|
if err := grant.IncreaseCounter(); err != nil {
|
|
return nil, &AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidGrant,
|
|
ErrorDescription: "cannot increase the grant counter",
|
|
}
|
|
}
|
|
}
|
|
// generate access token to access the API
|
|
expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
|
|
accessToken := &oauth2.Token{
|
|
GrantID: grant.ID,
|
|
Type: oauth2.TypeAccessToken,
|
|
StandardClaims: jwt.StandardClaims{
|
|
ExpiresAt: expirationDate.AsTime().Unix(),
|
|
},
|
|
}
|
|
signedAccessToken, err := accessToken.SignToken(serverKey)
|
|
if err != nil {
|
|
return nil, &AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "cannot sign token",
|
|
}
|
|
}
|
|
|
|
// generate refresh token to request an access token after it expired later
|
|
refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime().Unix()
|
|
refreshToken := &oauth2.Token{
|
|
GrantID: grant.ID,
|
|
Counter: grant.Counter,
|
|
Type: oauth2.TypeRefreshToken,
|
|
StandardClaims: jwt.StandardClaims{
|
|
ExpiresAt: refreshExpirationDate,
|
|
},
|
|
}
|
|
signedRefreshToken, err := refreshToken.SignToken(serverKey)
|
|
if err != nil {
|
|
return nil, &AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "cannot sign token",
|
|
}
|
|
}
|
|
|
|
// generate OpenID Connect id_token
|
|
signedIDToken := ""
|
|
if grant.ScopeContains("openid") {
|
|
app, err := login.GetOAuth2ApplicationByID(grant.ApplicationID)
|
|
if err != nil {
|
|
return nil, &AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "cannot find application",
|
|
}
|
|
}
|
|
user, err := models.GetUserByID(grant.UserID)
|
|
if err != nil {
|
|
if models.IsErrUserNotExist(err) {
|
|
return nil, &AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "cannot find user",
|
|
}
|
|
}
|
|
log.Error("Error loading user: %v", err)
|
|
return nil, &AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "server error",
|
|
}
|
|
}
|
|
|
|
idToken := &oauth2.OIDCToken{
|
|
StandardClaims: jwt.StandardClaims{
|
|
ExpiresAt: expirationDate.AsTime().Unix(),
|
|
Issuer: setting.AppURL,
|
|
Audience: app.ClientID,
|
|
Subject: fmt.Sprint(grant.UserID),
|
|
},
|
|
Nonce: grant.Nonce,
|
|
}
|
|
if grant.ScopeContains("profile") {
|
|
idToken.Name = user.FullName
|
|
idToken.PreferredUsername = user.Name
|
|
idToken.Profile = user.HTMLURL()
|
|
idToken.Picture = user.AvatarLink()
|
|
idToken.Website = user.Website
|
|
idToken.Locale = user.Language
|
|
idToken.UpdatedAt = user.UpdatedUnix
|
|
}
|
|
if grant.ScopeContains("email") {
|
|
idToken.Email = user.Email
|
|
idToken.EmailVerified = user.IsActive
|
|
}
|
|
if grant.ScopeContains("groups") {
|
|
groups, err := getOAuthGroupsForUser(user)
|
|
if err != nil {
|
|
log.Error("Error getting groups: %v", err)
|
|
return nil, &AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "server error",
|
|
}
|
|
}
|
|
idToken.Groups = groups
|
|
}
|
|
|
|
signedIDToken, err = idToken.SignToken(clientKey)
|
|
if err != nil {
|
|
return nil, &AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "cannot sign token",
|
|
}
|
|
}
|
|
}
|
|
|
|
return &AccessTokenResponse{
|
|
AccessToken: signedAccessToken,
|
|
TokenType: TokenTypeBearer,
|
|
ExpiresIn: setting.OAuth2.AccessTokenExpirationTime,
|
|
RefreshToken: signedRefreshToken,
|
|
IDToken: signedIDToken,
|
|
}, 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"`
|
|
Groups []string `json:"groups"`
|
|
}
|
|
|
|
// InfoOAuth manages request for userinfo endpoint
|
|
func InfoOAuth(ctx *context.Context) {
|
|
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(ctx.User.ID),
|
|
Name: ctx.User.FullName,
|
|
Username: ctx.User.Name,
|
|
Email: ctx.User.Email,
|
|
Picture: ctx.User.AvatarLink(),
|
|
}
|
|
|
|
groups, err := getOAuthGroupsForUser(ctx.User)
|
|
if err != nil {
|
|
ctx.ServerError("Oauth groups for user", err)
|
|
return
|
|
}
|
|
response.Groups = groups
|
|
|
|
ctx.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// returns a list of "org" and "org:team" strings,
|
|
// that the given user is a part of.
|
|
func getOAuthGroupsForUser(user *models.User) ([]string, error) {
|
|
orgs, err := models.GetUserOrgsList(user)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetUserOrgList: %v", err)
|
|
}
|
|
|
|
var groups []string
|
|
for _, org := range orgs {
|
|
groups = append(groups, org.Name)
|
|
|
|
if err := org.LoadTeams(); err != nil {
|
|
return nil, fmt.Errorf("LoadTeams: %v", err)
|
|
}
|
|
for _, team := range org.Teams {
|
|
if team.IsMember(user.ID) {
|
|
groups = append(groups, org.Name+":"+team.LowerName)
|
|
}
|
|
}
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
// 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, oauth2.DefaultSigningKey)
|
|
if err == nil {
|
|
if token.Valid() == nil {
|
|
grant, err := login.GetOAuth2GrantByID(token.GrantID)
|
|
if err == nil && grant != nil {
|
|
app, err := login.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)
|
|
errs := binding.Errors{}
|
|
errs = form.Validate(ctx.Req, errs)
|
|
if len(errs) > 0 {
|
|
errstring := ""
|
|
for _, e := range errs {
|
|
errstring += e.Error() + "\n"
|
|
}
|
|
ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
|
|
return
|
|
}
|
|
|
|
app, err := login.GetOAuth2ApplicationByClientID(form.ClientID)
|
|
if err != nil {
|
|
if login.IsErrOauthClientIDInvalid(err) {
|
|
handleAuthorizeError(ctx, AuthorizeError{
|
|
ErrorCode: ErrorCodeUnauthorizedClient,
|
|
ErrorDescription: "Client ID not registered",
|
|
State: form.State,
|
|
}, "")
|
|
return
|
|
}
|
|
ctx.ServerError("GetOAuth2ApplicationByClientID", err)
|
|
return
|
|
}
|
|
|
|
user, err := models.GetUserByID(app.UID)
|
|
if err != nil {
|
|
ctx.ServerError("GetUserByID", err)
|
|
return
|
|
}
|
|
|
|
if !app.ContainsRedirectURI(form.RedirectURI) {
|
|
handleAuthorizeError(ctx, AuthorizeError{
|
|
ErrorCode: ErrorCodeInvalidRequest,
|
|
ErrorDescription: "Unregistered Redirect URI",
|
|
State: form.State,
|
|
}, "")
|
|
return
|
|
}
|
|
|
|
if form.ResponseType != "code" {
|
|
handleAuthorizeError(ctx, AuthorizeError{
|
|
ErrorCode: ErrorCodeUnsupportedResponseType,
|
|
ErrorDescription: "Only code response type is supported.",
|
|
State: form.State,
|
|
}, form.RedirectURI)
|
|
return
|
|
}
|
|
|
|
// pkce support
|
|
switch form.CodeChallengeMethod {
|
|
case "S256":
|
|
case "plain":
|
|
if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
|
|
handleAuthorizeError(ctx, AuthorizeError{
|
|
ErrorCode: ErrorCodeServerError,
|
|
ErrorDescription: "cannot set code challenge method",
|
|
State: form.State,
|
|
}, form.RedirectURI)
|
|
return
|
|
}
|
|
if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallenge); err != nil {
|
|
handleAuthorizeError(ctx, AuthorizeError{
|
|
ErrorCode: ErrorCodeServerError,
|
|
ErrorDescription: "cannot set code challenge",
|
|
State: form.State,
|
|
}, form.RedirectURI)
|
|
return
|
|
}
|
|
// Here we're just going to try to release the session early
|
|
if err := ctx.Session.Release(); err != nil {
|
|
// we'll tolerate errors here as they *should* get saved elsewhere
|
|
log.Error("Unable to save changes to the session: %v", err)
|
|
}
|
|
case "":
|
|
break
|
|
default:
|
|
handleAuthorizeError(ctx, AuthorizeError{
|
|
ErrorCode: ErrorCodeInvalidRequest,
|
|
ErrorDescription: "unsupported code challenge method",
|
|
State: form.State,
|
|
}, form.RedirectURI)
|
|
return
|
|
}
|
|
|
|
grant, err := app.GetGrantByUserID(ctx.User.ID)
|
|
if err != nil {
|
|
handleServerError(ctx, form.State, form.RedirectURI)
|
|
return
|
|
}
|
|
|
|
// Redirect if user already granted access
|
|
if grant != nil {
|
|
code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
|
|
if err != nil {
|
|
handleServerError(ctx, form.State, form.RedirectURI)
|
|
return
|
|
}
|
|
redirect, err := code.GenerateRedirectURI(form.State)
|
|
if err != nil {
|
|
handleServerError(ctx, form.State, form.RedirectURI)
|
|
return
|
|
}
|
|
// Update nonce to reflect the new session
|
|
if len(form.Nonce) > 0 {
|
|
err := grant.SetNonce(form.Nonce)
|
|
if err != nil {
|
|
log.Error("Unable to update nonce: %v", err)
|
|
}
|
|
}
|
|
ctx.Redirect(redirect.String(), 302)
|
|
return
|
|
}
|
|
|
|
// show authorize page to grant access
|
|
ctx.Data["Application"] = app
|
|
ctx.Data["RedirectURI"] = form.RedirectURI
|
|
ctx.Data["State"] = form.State
|
|
ctx.Data["Scope"] = form.Scope
|
|
ctx.Data["Nonce"] = form.Nonce
|
|
ctx.Data["ApplicationUserLink"] = "<a href=\"" + html.EscapeString(user.HTMLURL()) + "\">@" + html.EscapeString(user.Name) + "</a>"
|
|
ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + html.EscapeString(form.RedirectURI) + "</strong>"
|
|
// TODO document SESSION <=> FORM
|
|
err = ctx.Session.Set("client_id", app.ClientID)
|
|
if err != nil {
|
|
handleServerError(ctx, form.State, form.RedirectURI)
|
|
log.Error(err.Error())
|
|
return
|
|
}
|
|
err = ctx.Session.Set("redirect_uri", form.RedirectURI)
|
|
if err != nil {
|
|
handleServerError(ctx, form.State, form.RedirectURI)
|
|
log.Error(err.Error())
|
|
return
|
|
}
|
|
err = ctx.Session.Set("state", form.State)
|
|
if err != nil {
|
|
handleServerError(ctx, form.State, form.RedirectURI)
|
|
log.Error(err.Error())
|
|
return
|
|
}
|
|
// Here we're just going to try to release the session early
|
|
if err := ctx.Session.Release(); err != nil {
|
|
// we'll tolerate errors here as they *should* get saved elsewhere
|
|
log.Error("Unable to save changes to the session: %v", err)
|
|
}
|
|
ctx.HTML(http.StatusOK, tplGrantAccess)
|
|
}
|
|
|
|
// GrantApplicationOAuth manages the post request submitted when a user grants access to an application
|
|
func GrantApplicationOAuth(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.GrantApplicationForm)
|
|
if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State ||
|
|
ctx.Session.Get("redirect_uri") != form.RedirectURI {
|
|
ctx.Error(http.StatusBadRequest)
|
|
return
|
|
}
|
|
app, err := login.GetOAuth2ApplicationByClientID(form.ClientID)
|
|
if err != nil {
|
|
ctx.ServerError("GetOAuth2ApplicationByClientID", err)
|
|
return
|
|
}
|
|
grant, err := app.CreateGrant(ctx.User.ID, form.Scope)
|
|
if err != nil {
|
|
handleAuthorizeError(ctx, AuthorizeError{
|
|
State: form.State,
|
|
ErrorDescription: "cannot create grant for user",
|
|
ErrorCode: ErrorCodeServerError,
|
|
}, form.RedirectURI)
|
|
return
|
|
}
|
|
if len(form.Nonce) > 0 {
|
|
err := grant.SetNonce(form.Nonce)
|
|
if err != nil {
|
|
log.Error("Unable to update nonce: %v", err)
|
|
}
|
|
}
|
|
|
|
var codeChallenge, codeChallengeMethod string
|
|
codeChallenge, _ = ctx.Session.Get("CodeChallenge").(string)
|
|
codeChallengeMethod, _ = ctx.Session.Get("CodeChallengeMethod").(string)
|
|
|
|
code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, codeChallenge, codeChallengeMethod)
|
|
if err != nil {
|
|
handleServerError(ctx, form.State, form.RedirectURI)
|
|
return
|
|
}
|
|
redirect, err := code.GenerateRedirectURI(form.State)
|
|
if err != nil {
|
|
handleServerError(ctx, form.State, form.RedirectURI)
|
|
return
|
|
}
|
|
ctx.Redirect(redirect.String(), 302)
|
|
}
|
|
|
|
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
|
|
func OIDCWellKnown(ctx *context.Context) {
|
|
t := ctx.Render.TemplateLookup("user/auth/oidc_wellknown")
|
|
ctx.Resp.Header().Set("Content-Type", "application/json")
|
|
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
|
|
if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
|
|
log.Error("%v", err)
|
|
ctx.Error(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// OIDCKeys generates the JSON Web Key Set
|
|
func OIDCKeys(ctx *context.Context) {
|
|
jwk, err := oauth2.DefaultSigningKey.ToJWK()
|
|
if err != nil {
|
|
log.Error("Error converting signing key to JWK: %v", err)
|
|
ctx.Error(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
jwk["use"] = "sig"
|
|
|
|
jwks := map[string][]map[string]string{
|
|
"keys": {
|
|
jwk,
|
|
},
|
|
}
|
|
|
|
ctx.Resp.Header().Set("Content-Type", "application/json")
|
|
enc := json.NewEncoder(ctx.Resp)
|
|
if err := enc.Encode(jwks); err != nil {
|
|
log.Error("Failed to encode representation as json. Error: %v", err)
|
|
}
|
|
}
|
|
|
|
// AccessTokenOAuth manages all access token requests by the client
|
|
func AccessTokenOAuth(ctx *context.Context) {
|
|
form := *web.GetForm(ctx).(*forms.AccessTokenForm)
|
|
if form.ClientID == "" {
|
|
authHeader := ctx.Req.Header.Get("Authorization")
|
|
authContent := strings.SplitN(authHeader, " ", 2)
|
|
if len(authContent) == 2 && authContent[0] == "Basic" {
|
|
payload, err := base64.StdEncoding.DecodeString(authContent[1])
|
|
if err != nil {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "cannot parse basic auth header",
|
|
})
|
|
return
|
|
}
|
|
pair := strings.SplitN(string(payload), ":", 2)
|
|
if len(pair) != 2 {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "cannot parse basic auth header",
|
|
})
|
|
return
|
|
}
|
|
form.ClientID = pair[0]
|
|
form.ClientSecret = pair[1]
|
|
}
|
|
}
|
|
|
|
serverKey := oauth2.DefaultSigningKey
|
|
clientKey := serverKey
|
|
if serverKey.IsSymmetric() {
|
|
var err error
|
|
clientKey, err = oauth2.CreateJWTSigningKey(serverKey.SigningMethod().Alg(), []byte(form.ClientSecret))
|
|
if err != nil {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "Error creating signing key",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
switch form.GrantType {
|
|
case "refresh_token":
|
|
handleRefreshToken(ctx, form, serverKey, clientKey)
|
|
case "authorization_code":
|
|
handleAuthorizationCode(ctx, form, serverKey, clientKey)
|
|
default:
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeUnsupportedGrantType,
|
|
ErrorDescription: "Only refresh_token or authorization_code grant type is supported",
|
|
})
|
|
}
|
|
}
|
|
|
|
func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2.JWTSigningKey) {
|
|
token, err := oauth2.ParseToken(form.RefreshToken, serverKey)
|
|
if err != nil {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
|
|
ErrorDescription: "client is not authorized",
|
|
})
|
|
return
|
|
}
|
|
// get grant before increasing counter
|
|
grant, err := login.GetOAuth2GrantByID(token.GrantID)
|
|
if err != nil || grant == nil {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidGrant,
|
|
ErrorDescription: "grant does not exist",
|
|
})
|
|
return
|
|
}
|
|
|
|
// check if token got already used
|
|
if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
|
|
ErrorDescription: "token was already used",
|
|
})
|
|
log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID)
|
|
return
|
|
}
|
|
accessToken, tokenErr := newAccessTokenResponse(grant, serverKey, clientKey)
|
|
if tokenErr != nil {
|
|
handleAccessTokenError(ctx, *tokenErr)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, accessToken)
|
|
}
|
|
|
|
func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2.JWTSigningKey) {
|
|
app, err := login.GetOAuth2ApplicationByClientID(form.ClientID)
|
|
if err != nil {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidClient,
|
|
ErrorDescription: fmt.Sprintf("cannot load client with client id: '%s'", form.ClientID),
|
|
})
|
|
return
|
|
}
|
|
if !app.ValidateClientSecret([]byte(form.ClientSecret)) {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
|
|
ErrorDescription: "client is not authorized",
|
|
})
|
|
return
|
|
}
|
|
if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
|
|
ErrorDescription: "client is not authorized",
|
|
})
|
|
return
|
|
}
|
|
authorizationCode, err := login.GetOAuth2AuthorizationByCode(form.Code)
|
|
if err != nil || authorizationCode == nil {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
|
|
ErrorDescription: "client is not authorized",
|
|
})
|
|
return
|
|
}
|
|
// check if code verifier authorizes the client, PKCE support
|
|
if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
|
|
ErrorDescription: "client is not authorized",
|
|
})
|
|
return
|
|
}
|
|
// check if granted for this application
|
|
if authorizationCode.Grant.ApplicationID != app.ID {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidGrant,
|
|
ErrorDescription: "invalid grant",
|
|
})
|
|
return
|
|
}
|
|
// remove token from database to deny duplicate usage
|
|
if err := authorizationCode.Invalidate(); err != nil {
|
|
handleAccessTokenError(ctx, AccessTokenError{
|
|
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
|
ErrorDescription: "cannot proceed your request",
|
|
})
|
|
}
|
|
resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, serverKey, clientKey)
|
|
if tokenErr != nil {
|
|
handleAccessTokenError(ctx, *tokenErr)
|
|
return
|
|
}
|
|
// send successful response
|
|
ctx.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
func handleAccessTokenError(ctx *context.Context, acErr AccessTokenError) {
|
|
ctx.JSON(http.StatusBadRequest, acErr)
|
|
}
|
|
|
|
func handleServerError(ctx *context.Context, state string, redirectURI string) {
|
|
handleAuthorizeError(ctx, AuthorizeError{
|
|
ErrorCode: ErrorCodeServerError,
|
|
ErrorDescription: "A server error occurred",
|
|
State: state,
|
|
}, redirectURI)
|
|
}
|
|
|
|
func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirectURI string) {
|
|
if redirectURI == "" {
|
|
log.Warn("Authorization failed: %v", authErr.ErrorDescription)
|
|
ctx.Data["Error"] = authErr
|
|
ctx.HTML(400, tplGrantError)
|
|
return
|
|
}
|
|
redirect, err := url.Parse(redirectURI)
|
|
if err != nil {
|
|
ctx.ServerError("url.Parse", err)
|
|
return
|
|
}
|
|
q := redirect.Query()
|
|
q.Set("error", string(authErr.ErrorCode))
|
|
q.Set("error_description", authErr.ErrorDescription)
|
|
q.Set("state", authErr.State)
|
|
redirect.RawQuery = q.Encode()
|
|
ctx.Redirect(redirect.String(), 302)
|
|
}
|