Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
513 lines
17 KiB
513 lines
17 KiB
// 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/url" |
|
"strings" |
|
|
|
"code.gitea.io/gitea/models" |
|
"code.gitea.io/gitea/modules/auth" |
|
"code.gitea.io/gitea/modules/base" |
|
"code.gitea.io/gitea/modules/context" |
|
"code.gitea.io/gitea/modules/log" |
|
"code.gitea.io/gitea/modules/setting" |
|
"code.gitea.io/gitea/modules/timeutil" |
|
|
|
"gitea.com/macaron/binding" |
|
"github.com/dgrijalva/jwt-go" |
|
) |
|
|
|
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"` |
|
} |
|
|
|
func newAccessTokenResponse(grant *models.OAuth2Grant) (*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 := &models.OAuth2Token{ |
|
GrantID: grant.ID, |
|
Type: models.TypeAccessToken, |
|
StandardClaims: jwt.StandardClaims{ |
|
ExpiresAt: expirationDate.AsTime().Unix(), |
|
}, |
|
} |
|
signedAccessToken, err := accessToken.SignToken() |
|
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 := &models.OAuth2Token{ |
|
GrantID: grant.ID, |
|
Counter: grant.Counter, |
|
Type: models.TypeRefreshToken, |
|
StandardClaims: jwt.StandardClaims{ |
|
ExpiresAt: refreshExpirationDate, |
|
}, |
|
} |
|
signedRefreshToken, err := refreshToken.SignToken() |
|
if err != nil { |
|
return nil, &AccessTokenError{ |
|
ErrorCode: AccessTokenErrorCodeInvalidRequest, |
|
ErrorDescription: "cannot sign token", |
|
} |
|
} |
|
|
|
return &AccessTokenResponse{ |
|
AccessToken: signedAccessToken, |
|
TokenType: TokenTypeBearer, |
|
ExpiresIn: setting.OAuth2.AccessTokenExpirationTime, |
|
RefreshToken: signedRefreshToken, |
|
}, nil |
|
} |
|
|
|
// AuthorizeOAuth manages authorize requests |
|
func AuthorizeOAuth(ctx *context.Context, form auth.AuthorizationForm) { |
|
errs := binding.Errors{} |
|
errs = form.Validate(ctx.Context, 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 := models.GetOAuth2ApplicationByClientID(form.ClientID) |
|
if err != nil { |
|
if models.IsErrOauthClientIDInvalid(err) { |
|
handleAuthorizeError(ctx, AuthorizeError{ |
|
ErrorCode: ErrorCodeUnauthorizedClient, |
|
ErrorDescription: "Client ID not registered", |
|
State: form.State, |
|
}, "") |
|
return |
|
} |
|
ctx.ServerError("GetOAuth2ApplicationByClientID", err) |
|
return |
|
} |
|
if err := app.LoadUser(); err != nil { |
|
ctx.ServerError("LoadUser", 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 |
|
} |
|
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["ApplicationUserLink"] = "<a href=\"" + html.EscapeString(setting.AppURL) + html.EscapeString(url.PathEscape(app.User.LowerName)) + "\">@" + html.EscapeString(app.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(200, tplGrantAccess) |
|
} |
|
|
|
// GrantApplicationOAuth manages the post request submitted when a user grants access to an application |
|
func GrantApplicationOAuth(ctx *context.Context, form auth.GrantApplicationForm) { |
|
if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State || |
|
ctx.Session.Get("redirect_uri") != form.RedirectURI { |
|
ctx.Error(400) |
|
return |
|
} |
|
app, err := models.GetOAuth2ApplicationByClientID(form.ClientID) |
|
if err != nil { |
|
ctx.ServerError("GetOAuth2ApplicationByClientID", err) |
|
return |
|
} |
|
grant, err := app.CreateGrant(ctx.User.ID) |
|
if err != nil { |
|
handleAuthorizeError(ctx, AuthorizeError{ |
|
State: form.State, |
|
ErrorDescription: "cannot create grant for user", |
|
ErrorCode: ErrorCodeServerError, |
|
}, form.RedirectURI) |
|
return |
|
} |
|
|
|
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) |
|
} |
|
|
|
// AccessTokenOAuth manages all access token requests by the client |
|
func AccessTokenOAuth(ctx *context.Context, form auth.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] |
|
} |
|
} |
|
switch form.GrantType { |
|
case "refresh_token": |
|
handleRefreshToken(ctx, form) |
|
return |
|
case "authorization_code": |
|
handleAuthorizationCode(ctx, form) |
|
return |
|
default: |
|
handleAccessTokenError(ctx, AccessTokenError{ |
|
ErrorCode: AccessTokenErrorCodeUnsupportedGrantType, |
|
ErrorDescription: "Only refresh_token or authorization_code grant type is supported", |
|
}) |
|
} |
|
} |
|
|
|
func handleRefreshToken(ctx *context.Context, form auth.AccessTokenForm) { |
|
token, err := models.ParseOAuth2Token(form.RefreshToken) |
|
if err != nil { |
|
handleAccessTokenError(ctx, AccessTokenError{ |
|
ErrorCode: AccessTokenErrorCodeUnauthorizedClient, |
|
ErrorDescription: "client is not authorized", |
|
}) |
|
return |
|
} |
|
// get grant before increasing counter |
|
grant, err := models.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) |
|
if tokenErr != nil { |
|
handleAccessTokenError(ctx, *tokenErr) |
|
return |
|
} |
|
ctx.JSON(200, accessToken) |
|
} |
|
|
|
func handleAuthorizationCode(ctx *context.Context, form auth.AccessTokenForm) { |
|
app, err := models.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 := models.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) |
|
if tokenErr != nil { |
|
handleAccessTokenError(ctx, *tokenErr) |
|
return |
|
} |
|
// send successful response |
|
ctx.JSON(200, resp) |
|
} |
|
|
|
func handleAccessTokenError(ctx *context.Context, acErr AccessTokenError) { |
|
ctx.JSON(400, 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) |
|
}
|
|
|