casdoor--casdoor
836 行
24 KiB
Go
836 行
24 KiB
Go
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package object
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/casdoor/casdoor/i18n"
|
|
"github.com/casdoor/casdoor/util"
|
|
"github.com/xorm-io/core"
|
|
)
|
|
|
|
const (
|
|
hourSeconds = int(time.Hour / time.Second)
|
|
InvalidRequest = "invalid_request"
|
|
InvalidClient = "invalid_client"
|
|
InvalidGrant = "invalid_grant"
|
|
UnauthorizedClient = "unauthorized_client"
|
|
UnsupportedGrantType = "unsupported_grant_type"
|
|
InvalidScope = "invalid_scope"
|
|
EndpointError = "endpoint_error"
|
|
DeviceAuthExpiresIn = 120
|
|
DeviceAuthInterval = 5
|
|
|
|
DeviceAuthStatusPending = "pending"
|
|
DeviceAuthStatusApproved = "approved"
|
|
DeviceAuthStatusDenied = "denied"
|
|
DeviceAuthStatusTokenIssued = "token_issued"
|
|
)
|
|
|
|
// DeviceAuthMap stores the transient state of the OAuth 2.0 Device Authorization Grant (RFC 8628).
|
|
// It defaults to an in-memory store; call InitDeviceAuthStore() at startup to switch to Redis
|
|
// when redisEndpoint is configured, enabling correct behaviour across multiple replicas.
|
|
var DeviceAuthMap deviceAuthStore = &memoryDeviceAuthStore{}
|
|
|
|
type Code struct {
|
|
Message string `xorm:"varchar(100)" json:"message"`
|
|
Code string `xorm:"varchar(100)" json:"code"`
|
|
}
|
|
|
|
type TokenWrapper struct {
|
|
AccessToken string `json:"access_token"`
|
|
IdToken string `json:"id_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
TokenType string `json:"token_type"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
Scope string `json:"scope"`
|
|
}
|
|
|
|
type TokenError struct {
|
|
Error string `json:"error"`
|
|
ErrorDescription string `json:"error_description,omitempty"`
|
|
}
|
|
|
|
// DPoPConfirmation holds the DPoP key confirmation claim (RFC 9449).
|
|
type DPoPConfirmation struct {
|
|
JKT string `json:"jkt"`
|
|
}
|
|
|
|
type IntrospectionResponse struct {
|
|
Active bool `json:"active"`
|
|
Scope string `json:"scope,omitempty"`
|
|
ClientId string `json:"client_id,omitempty"`
|
|
Username string `json:"username,omitempty"`
|
|
TokenType string `json:"token_type,omitempty"`
|
|
Exp int64 `json:"exp,omitempty"`
|
|
Iat int64 `json:"iat,omitempty"`
|
|
Nbf int64 `json:"nbf,omitempty"`
|
|
Sub string `json:"sub,omitempty"`
|
|
Aud []string `json:"aud,omitempty"`
|
|
Iss string `json:"iss,omitempty"`
|
|
Jti string `json:"jti,omitempty"`
|
|
Cnf *DPoPConfirmation `json:"cnf,omitempty"` // RFC 9449 DPoP key binding
|
|
}
|
|
|
|
type DeviceAuthCache struct {
|
|
UserSignIn bool
|
|
UserName string
|
|
ApplicationId string
|
|
ClientId string
|
|
Scope string
|
|
RequestAt time.Time
|
|
Status string
|
|
CancelToken string
|
|
ExpiresIn int
|
|
}
|
|
|
|
func InitCleanupDeviceAuthMap() {
|
|
InitDeviceAuthStore()
|
|
util.SafeGoroutine(func() {
|
|
ticker := time.NewTicker(time.Minute)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
now := time.Now()
|
|
DeviceAuthMap.Range(func(key, value any) bool {
|
|
cache := value.(DeviceAuthCache)
|
|
expiresIn := cache.ExpiresIn
|
|
if expiresIn == 0 {
|
|
expiresIn = DeviceAuthExpiresIn
|
|
}
|
|
if cache.RequestAt.Add(time.Duration(expiresIn) * time.Second).Before(now) {
|
|
DeviceAuthMap.Delete(key)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
type DeviceAuthResponse struct {
|
|
DeviceCode string `json:"device_code"`
|
|
UserCode string `json:"user_code"`
|
|
VerificationUri string `json:"verification_uri"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
Interval int `json:"interval"`
|
|
}
|
|
|
|
// validateResourceURI validates that the resource parameter is a valid absolute URI
|
|
// according to RFC 8707 Section 2
|
|
func validateResourceURI(resource string) error {
|
|
if resource == "" {
|
|
return nil // empty resource is allowed (backward compatibility)
|
|
}
|
|
|
|
parsedURL, err := url.Parse(resource)
|
|
if err != nil {
|
|
return fmt.Errorf("resource must be a valid URI")
|
|
}
|
|
|
|
// RFC 8707: The resource parameter must be an absolute URI
|
|
if !parsedURL.IsAbs() {
|
|
return fmt.Errorf("resource must be an absolute URI")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// pkceChallenge returns the base64-URL-encoded SHA256 hash of verifier, per RFC 7636
|
|
func pkceChallenge(verifier string) string {
|
|
sum := sha256.Sum256([]byte(verifier))
|
|
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(sum[:])
|
|
}
|
|
|
|
// IsGrantTypeValid checks if grantType is allowed in the current application.
|
|
// authorization_code is allowed by default.
|
|
func IsGrantTypeValid(method string, grantTypes []string) bool {
|
|
if method == "authorization_code" {
|
|
return true
|
|
}
|
|
for _, m := range grantTypes {
|
|
if m == method {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isRegexScope returns true if the scope string contains regex metacharacters.
|
|
func isRegexScope(scope string) bool {
|
|
return strings.ContainsAny(scope, ".*+?^${}()|[]\\")
|
|
}
|
|
|
|
// IsScopeValidAndExpand expands any regex patterns in the space-separated scope string
|
|
// against the application's configured scopes. Literal scopes are kept as-is
|
|
// after verifying they exist in the allowed list. Regex scopes are matched
|
|
// against every allowed scope name; all matches replace the pattern.
|
|
// If the application has no defined scopes, the original scope string is
|
|
// returned unchanged (backward-compatible behaviour).
|
|
// Returns the expanded scope string and whether the scope is valid.
|
|
func IsScopeValidAndExpand(scope string, application *Application) (string, bool) {
|
|
if len(application.Scopes) == 0 || scope == "" {
|
|
return scope, true
|
|
}
|
|
|
|
allowedNames := make([]string, 0, len(application.Scopes))
|
|
allowedSet := make(map[string]bool, len(application.Scopes))
|
|
for _, s := range application.Scopes {
|
|
allowedNames = append(allowedNames, s.Name)
|
|
allowedSet[s.Name] = true
|
|
}
|
|
|
|
seen := make(map[string]bool)
|
|
var expanded []string
|
|
|
|
for _, s := range strings.Fields(scope) {
|
|
// Try exact match first.
|
|
if allowedSet[s] {
|
|
if !seen[s] {
|
|
seen[s] = true
|
|
expanded = append(expanded, s)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Not an exact match – if it looks like a regex, try pattern matching.
|
|
if !isRegexScope(s) {
|
|
return "", false
|
|
}
|
|
|
|
// Treat as regex pattern – must be a valid regex and match ≥ 1 scope.
|
|
re, err := regexp.Compile("^" + s + "$")
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
matched := false
|
|
for _, name := range allowedNames {
|
|
if re.MatchString(name) {
|
|
matched = true
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
expanded = append(expanded, name)
|
|
}
|
|
}
|
|
}
|
|
if !matched {
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
return strings.Join(expanded, " "), true
|
|
}
|
|
|
|
// IsScopeValid checks whether all space-separated scopes in the scope string
|
|
// are defined in the application's Scopes list (including regex expansion).
|
|
// If the application has no defined scopes, every scope is considered valid
|
|
// (backward-compatible behaviour).
|
|
func IsScopeValid(scope string, application *Application) bool {
|
|
_, ok := IsScopeValidAndExpand(scope, application)
|
|
return ok
|
|
}
|
|
|
|
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
|
|
token, err := GetTokenByAccessToken(accessToken)
|
|
if err != nil {
|
|
return false, nil, nil, err
|
|
}
|
|
if token == nil {
|
|
return false, nil, nil, nil
|
|
}
|
|
|
|
token.ExpiresIn = 0
|
|
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Cols("expires_in").Update(token)
|
|
if err != nil {
|
|
return false, nil, nil, err
|
|
}
|
|
|
|
application, err := getApplication(token.Owner, token.Application)
|
|
if err != nil {
|
|
return false, nil, nil, err
|
|
}
|
|
|
|
return affected != 0, application, token, nil
|
|
}
|
|
|
|
func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string, lang string) (string, *Application, error) {
|
|
if responseType != "code" && responseType != "token" && responseType != "id_token" {
|
|
return fmt.Sprintf(i18n.Translate(lang, "token:Grant_type: %s is not supported in this application"), responseType), nil, nil
|
|
}
|
|
|
|
application, err := GetApplicationByClientId(clientId)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
if application == nil {
|
|
return i18n.Translate(lang, "token:Invalid client_id"), nil, nil
|
|
}
|
|
|
|
if !application.IsRedirectUriValid(redirectUri) {
|
|
return fmt.Sprintf(i18n.Translate(lang, "token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri), application, nil
|
|
}
|
|
|
|
if !IsScopeValid(scope, application) {
|
|
return i18n.Translate(lang, "token:Invalid scope"), application, nil
|
|
}
|
|
|
|
// Mask application for /api/get-app-login
|
|
application.ClientSecret = ""
|
|
return "", application, nil
|
|
}
|
|
|
|
func GetOAuthCode(userId string, clientId string, provider string, signinMethod string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, resource string, host string, lang string) (*Code, error) {
|
|
user, err := GetUser(userId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user == nil {
|
|
return &Code{
|
|
Message: fmt.Sprintf("general:The user: %s doesn't exist", userId),
|
|
Code: "",
|
|
}, nil
|
|
}
|
|
if user.IsForbidden {
|
|
return &Code{
|
|
Message: "error: the user is forbidden to sign in, please contact the administrator",
|
|
Code: "",
|
|
}, nil
|
|
}
|
|
|
|
msg, application, err := CheckOAuthLogin(clientId, responseType, redirectUri, scope, state, lang)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if msg != "" {
|
|
return &Code{
|
|
Message: msg,
|
|
Code: "",
|
|
}, nil
|
|
}
|
|
|
|
// Expand regex/wildcard scopes to concrete scope names.
|
|
expandedScope, ok := IsScopeValidAndExpand(scope, application)
|
|
if !ok {
|
|
return &Code{
|
|
Message: i18n.Translate(lang, "token:Invalid scope"),
|
|
Code: "",
|
|
}, nil
|
|
}
|
|
scope = expandedScope
|
|
|
|
// Validate resource parameter (RFC 8707)
|
|
if err := validateResourceURI(resource); err != nil {
|
|
return &Code{
|
|
Message: err.Error(),
|
|
Code: "",
|
|
}, nil
|
|
}
|
|
|
|
err = ExtendUserWithRolesAndPermissions(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, signinMethod, nonce, scope, resource, host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if challenge == "null" {
|
|
challenge = ""
|
|
}
|
|
|
|
token := &Token{
|
|
Owner: application.Owner,
|
|
Name: tokenName,
|
|
CreatedTime: util.GetCurrentTime(),
|
|
Application: application.Name,
|
|
Organization: user.Owner,
|
|
User: user.Name,
|
|
Code: util.GenerateClientId(),
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
ExpiresIn: int(application.ExpireInHours * float64(hourSeconds)),
|
|
Scope: scope,
|
|
TokenType: "Bearer",
|
|
CodeChallenge: challenge,
|
|
CodeIsUsed: false,
|
|
CodeExpireIn: time.Now().Add(time.Minute * 5).Unix(),
|
|
Resource: resource,
|
|
}
|
|
_, err = AddToken(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Code{
|
|
Message: "",
|
|
Code: token.Code,
|
|
}, nil
|
|
}
|
|
|
|
func RefreshToken(application *Application, grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string, dpopProof string) (interface{}, error) {
|
|
if grantType != "refresh_token" {
|
|
return &TokenError{
|
|
Error: UnsupportedGrantType,
|
|
ErrorDescription: "grant_type should be refresh_token",
|
|
}, nil
|
|
}
|
|
|
|
var err error
|
|
if application == nil {
|
|
application, err = GetApplicationByClientId(clientId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if application == nil {
|
|
return &TokenError{
|
|
Error: InvalidClient,
|
|
ErrorDescription: "client_id is invalid",
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
if clientSecret != "" && application.ClientSecret != clientSecret {
|
|
return &TokenError{
|
|
Error: InvalidClient,
|
|
ErrorDescription: "client_secret is invalid",
|
|
}, nil
|
|
}
|
|
|
|
// check whether the refresh token is valid, and has not expired.
|
|
token, err := GetTokenByRefreshToken(refreshToken)
|
|
if err != nil || token == nil {
|
|
return &TokenError{
|
|
Error: InvalidGrant,
|
|
ErrorDescription: "refresh token is invalid or revoked",
|
|
}, nil
|
|
}
|
|
|
|
// check if the token has been invalidated (e.g., by SSO logout)
|
|
if token.ExpiresIn <= 0 {
|
|
return &TokenError{
|
|
Error: InvalidGrant,
|
|
ErrorDescription: "refresh token is expired",
|
|
}, nil
|
|
}
|
|
|
|
cert, err := getCertByApplication(application)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cert == nil {
|
|
return &TokenError{
|
|
Error: InvalidGrant,
|
|
ErrorDescription: fmt.Sprintf("cert: %s cannot be found", application.Cert),
|
|
}, nil
|
|
}
|
|
|
|
var oldTokenScope string
|
|
if application.TokenFormat == "JWT-Standard" {
|
|
oldToken, err := ParseStandardJwtToken(refreshToken, cert)
|
|
if err != nil {
|
|
return &TokenError{
|
|
Error: InvalidGrant,
|
|
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
oldTokenScope = oldToken.Scope
|
|
} else {
|
|
oldToken, err := ParseJwtToken(refreshToken, cert)
|
|
if err != nil {
|
|
return &TokenError{
|
|
Error: InvalidGrant,
|
|
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
oldTokenScope = oldToken.Scope
|
|
}
|
|
|
|
if scope == "" {
|
|
scope = oldTokenScope
|
|
}
|
|
|
|
// generate a new token
|
|
user, err := getUser(application.Organization, token.User)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if user == nil {
|
|
return "", fmt.Errorf("The user: %s doesn't exist", util.GetId(application.Organization, token.User))
|
|
}
|
|
|
|
if user.IsForbidden {
|
|
return &TokenError{
|
|
Error: InvalidGrant,
|
|
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
|
|
}, nil
|
|
}
|
|
|
|
err = ExtendUserWithRolesAndPermissions(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, "", host)
|
|
if err != nil {
|
|
return &TokenError{
|
|
Error: EndpointError,
|
|
ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
|
|
newToken := &Token{
|
|
Owner: application.Owner,
|
|
Name: tokenName,
|
|
CreatedTime: util.GetCurrentTime(),
|
|
Application: application.Name,
|
|
Organization: user.Owner,
|
|
User: user.Name,
|
|
Code: util.GenerateClientId(),
|
|
AccessToken: newAccessToken,
|
|
RefreshToken: newRefreshToken,
|
|
ExpiresIn: int(application.ExpireInHours * float64(hourSeconds)),
|
|
Scope: scope,
|
|
TokenType: "Bearer",
|
|
}
|
|
_, err = AddToken(newToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Apply DPoP binding to the refreshed token if a DPoP proof was provided.
|
|
if dpopProof != "" {
|
|
dpopHtu := GetDPoPHtu(host, "/api/login/oauth/access_token")
|
|
jkt, err := ValidateDPoPProof(dpopProof, "POST", dpopHtu, "")
|
|
if err != nil {
|
|
return &TokenError{
|
|
Error: "invalid_dpop_proof",
|
|
ErrorDescription: err.Error(),
|
|
}, nil
|
|
}
|
|
newToken.TokenType = "DPoP"
|
|
newToken.DPoPJkt = jkt
|
|
if err = updateTokenDPoP(newToken); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
_, err = DeleteToken(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tokenWrapper := &TokenWrapper{
|
|
AccessToken: newToken.AccessToken,
|
|
IdToken: newToken.AccessToken,
|
|
RefreshToken: newToken.RefreshToken,
|
|
TokenType: newToken.TokenType,
|
|
ExpiresIn: newToken.ExpiresIn,
|
|
Scope: newToken.Scope,
|
|
}
|
|
return tokenWrapper, nil
|
|
}
|
|
|
|
func ValidateJwtAssertion(clientAssertion string, application *Application, host string) (bool, *Claims, error) {
|
|
_, originBackend := getOriginFromHost(host)
|
|
|
|
clientCert, err := getCert(application.Owner, application.ClientCert)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
if clientCert == nil {
|
|
return false, nil, fmt.Errorf("client certificate is not configured for application: [%s]", application.GetId())
|
|
}
|
|
|
|
claims, err := ParseJwtToken(clientAssertion, clientCert)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
if !slices.Contains(application.RedirectUris, claims.Issuer) {
|
|
return false, nil, nil
|
|
}
|
|
|
|
if !slices.Contains(claims.Audience, fmt.Sprintf("%s/api/login/oauth/access_token", originBackend)) {
|
|
return false, nil, nil
|
|
}
|
|
|
|
return true, claims, nil
|
|
}
|
|
|
|
func ValidateClientAssertion(clientAssertion string, host string) (bool, *Application, error) {
|
|
token, err := ParseJwtTokenWithoutValidation(clientAssertion)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
clientId, err := token.Claims.GetSubject()
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
application, err := GetApplicationByClientId(clientId)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
if application == nil {
|
|
return false, nil, fmt.Errorf("application not found for client: [%s]", clientId)
|
|
}
|
|
|
|
ok, _, err := ValidateJwtAssertion(clientAssertion, application, host)
|
|
if err != nil {
|
|
return false, application, err
|
|
}
|
|
if !ok {
|
|
return false, application, nil
|
|
}
|
|
|
|
return true, application, nil
|
|
}
|
|
|
|
// mintImplicitToken mints a token for an already-authenticated user.
|
|
// Callers must verify user identity before calling this function.
|
|
func mintImplicitToken(application *Application, username string, scope string, nonce string, host string) (*Token, *TokenError, error) {
|
|
expandedScope, ok := IsScopeValidAndExpand(scope, application)
|
|
if !ok {
|
|
return nil, &TokenError{
|
|
Error: InvalidScope,
|
|
ErrorDescription: "the requested scope is invalid or not defined in the application",
|
|
}, nil
|
|
}
|
|
scope = expandedScope
|
|
|
|
user, err := GetUserByFieldsForSharedApp(application, application.Organization, username)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if user == nil {
|
|
return nil, &TokenError{
|
|
Error: InvalidGrant,
|
|
ErrorDescription: "the user does not exist",
|
|
}, nil
|
|
}
|
|
if user.IsForbidden {
|
|
return nil, &TokenError{
|
|
Error: InvalidGrant,
|
|
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
|
|
}, nil
|
|
}
|
|
|
|
token, err := GetTokenByUser(application, user, scope, nonce, host)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return token, nil, nil
|
|
}
|
|
|
|
// parseAndValidateSubjectToken validates a subject_token for RFC 8693 token exchange.
|
|
// It uses the ISSUING application's certificate (not the requesting client's) and
|
|
// enforces audience binding to prevent cross-client token reuse.
|
|
func parseAndValidateSubjectToken(subjectToken string, requestingClientId string) (owner, name, scope string, tokenErr *TokenError, err error) {
|
|
unverifiedToken, err := ParseJwtTokenWithoutValidation(subjectToken)
|
|
if err != nil {
|
|
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: fmt.Sprintf("invalid subject_token: %s", err.Error())}, nil
|
|
}
|
|
|
|
unverifiedClaims, ok := unverifiedToken.Claims.(*Claims)
|
|
if !ok || unverifiedClaims.Azp == "" {
|
|
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: "subject_token is missing the azp claim"}, nil
|
|
}
|
|
|
|
issuingApp, err := GetApplicationByClientId(unverifiedClaims.Azp)
|
|
if err != nil {
|
|
return "", "", "", nil, err
|
|
}
|
|
if issuingApp == nil {
|
|
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: fmt.Sprintf("subject_token issuing application not found: %s", unverifiedClaims.Azp)}, nil
|
|
}
|
|
|
|
cert, err := getCertByApplication(issuingApp)
|
|
if err != nil {
|
|
return "", "", "", nil, err
|
|
}
|
|
if cert == nil {
|
|
return "", "", "", &TokenError{Error: EndpointError, ErrorDescription: fmt.Sprintf("cert for issuing application %s cannot be found", unverifiedClaims.Azp)}, nil
|
|
}
|
|
|
|
if issuingApp.TokenFormat == "JWT-Standard" {
|
|
standardClaims, err := ParseStandardJwtToken(subjectToken, cert)
|
|
if err != nil {
|
|
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: fmt.Sprintf("invalid subject_token: %s", err.Error())}, nil
|
|
}
|
|
return standardClaims.Owner, standardClaims.Name, standardClaims.Scope, nil, nil
|
|
}
|
|
|
|
claims, err := ParseJwtToken(subjectToken, cert)
|
|
if err != nil {
|
|
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: fmt.Sprintf("invalid subject_token: %s", err.Error())}, nil
|
|
}
|
|
|
|
// Audience binding: requesting client must be the issuer itself or appear in token's aud.
|
|
// Prevents an attacker from exchanging App A's token to obtain an App B token (RFC 8693 §2.1).
|
|
if issuingApp.ClientId != requestingClientId {
|
|
audienceMatched := false
|
|
for _, aud := range claims.Audience {
|
|
if aud == requestingClientId {
|
|
audienceMatched = true
|
|
break
|
|
}
|
|
}
|
|
if !audienceMatched {
|
|
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: fmt.Sprintf("subject_token audience does not include the requesting client '%s'", requestingClientId)}, nil
|
|
}
|
|
}
|
|
|
|
return claims.Owner, claims.Name, claims.Scope, nil, nil
|
|
}
|
|
|
|
// createGuestUserToken creates a new guest user and returns a token for them.
|
|
func createGuestUserToken(application *Application, clientSecret string, verifier string) (*Token, *TokenError, error) {
|
|
if clientSecret != "" && application.ClientSecret != clientSecret {
|
|
return nil, &TokenError{
|
|
Error: InvalidClient,
|
|
ErrorDescription: "client_secret is invalid",
|
|
}, nil
|
|
}
|
|
|
|
guestUsername := generateGuestUsername()
|
|
guestPassword := util.GenerateId()
|
|
|
|
organization, err := GetOrganization(util.GetId("admin", application.Organization))
|
|
if err != nil {
|
|
return nil, &TokenError{
|
|
Error: EndpointError,
|
|
ErrorDescription: fmt.Sprintf("failed to get organization: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
if organization == nil {
|
|
return nil, &TokenError{
|
|
Error: InvalidClient,
|
|
ErrorDescription: fmt.Sprintf("organization: %s does not exist", application.Organization),
|
|
}, nil
|
|
}
|
|
|
|
initScore, err := organization.GetInitScore()
|
|
if err != nil {
|
|
return nil, &TokenError{
|
|
Error: EndpointError,
|
|
ErrorDescription: fmt.Sprintf("failed to get init score: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
|
|
newUserId, idErr := GenerateIdForNewUser(application)
|
|
if idErr != nil {
|
|
newUserId = util.GenerateId()
|
|
}
|
|
|
|
guestUser := &User{
|
|
Owner: application.Organization,
|
|
Name: guestUsername,
|
|
CreatedTime: util.GetCurrentTime(),
|
|
Id: newUserId,
|
|
Type: "normal-user",
|
|
Password: guestPassword,
|
|
Tag: "guest-user",
|
|
DisplayName: fmt.Sprintf("Guest_%s", guestUsername[:8]),
|
|
Avatar: "",
|
|
Address: []string{},
|
|
Email: "",
|
|
Phone: "",
|
|
Score: initScore,
|
|
IsAdmin: false,
|
|
IsForbidden: false,
|
|
IsDeleted: false,
|
|
SignupApplication: application.Name,
|
|
Properties: map[string]string{},
|
|
RegisterType: "Guest Signup",
|
|
RegisterSource: fmt.Sprintf("%s/%s", application.Organization, application.Name),
|
|
}
|
|
|
|
affected, err := AddUser(guestUser, "en")
|
|
if err != nil {
|
|
return nil, &TokenError{
|
|
Error: EndpointError,
|
|
ErrorDescription: fmt.Sprintf("failed to create guest user: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
if !affected {
|
|
return nil, &TokenError{
|
|
Error: EndpointError,
|
|
ErrorDescription: "failed to create guest user",
|
|
}, nil
|
|
}
|
|
|
|
err = ExtendUserWithRolesAndPermissions(guestUser)
|
|
if err != nil {
|
|
return nil, &TokenError{
|
|
Error: EndpointError,
|
|
ErrorDescription: fmt.Sprintf("failed to extend user: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
|
|
accessToken, refreshToken, tokenName, err := generateJwtToken(application, guestUser, "", "", "", "", "", "")
|
|
if err != nil {
|
|
return nil, &TokenError{
|
|
Error: EndpointError,
|
|
ErrorDescription: fmt.Sprintf("failed to generate token: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
|
|
token := &Token{
|
|
Owner: application.Owner,
|
|
Name: tokenName,
|
|
CreatedTime: util.GetCurrentTime(),
|
|
Application: application.Name,
|
|
Organization: guestUser.Owner,
|
|
User: guestUser.Name,
|
|
Code: util.GenerateClientId(),
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
ExpiresIn: int(application.ExpireInHours * float64(hourSeconds)),
|
|
Scope: "",
|
|
TokenType: "Bearer",
|
|
CodeChallenge: "",
|
|
CodeIsUsed: true,
|
|
CodeExpireIn: 0,
|
|
}
|
|
|
|
_, err = AddToken(token)
|
|
if err != nil {
|
|
return nil, &TokenError{
|
|
Error: EndpointError,
|
|
ErrorDescription: fmt.Sprintf("failed to add token: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
|
|
return token, nil, nil
|
|
}
|
|
|
|
// generateGuestUsername generates a unique username for guest users.
|
|
func generateGuestUsername() string {
|
|
return fmt.Sprintf("guest_%s", util.GenerateUUID())
|
|
}
|