OAuth2 provider: support for granular scopes
- `CheckOAuthAccessToken` returns both user ID and additional scopes - `grantAdditionalScopes` returns AccessTokenScope ready string (grantScopes) compiled from requested additional scopes by the client - `userIDFromToken` sets returned grantScopes (if any) instead of default `all`
This commit is contained in:
parent
3301e7dc75
commit
4eb8d8c496
4 changed files with 76 additions and 26 deletions
|
@ -92,23 +92,25 @@ func parseScopes(sec ConfigSection, name string) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
var OAuth2 = struct {
|
var OAuth2 = struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
AccessTokenExpirationTime int64
|
AccessTokenExpirationTime int64
|
||||||
RefreshTokenExpirationTime int64
|
RefreshTokenExpirationTime int64
|
||||||
InvalidateRefreshTokens bool
|
InvalidateRefreshTokens bool
|
||||||
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
|
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
|
||||||
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
|
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
|
||||||
MaxTokenLength int
|
MaxTokenLength int
|
||||||
DefaultApplications []string
|
DefaultApplications []string
|
||||||
|
EnableAdditionalGrantScopes bool
|
||||||
}{
|
}{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
AccessTokenExpirationTime: 3600,
|
AccessTokenExpirationTime: 3600,
|
||||||
RefreshTokenExpirationTime: 730,
|
RefreshTokenExpirationTime: 730,
|
||||||
InvalidateRefreshTokens: true,
|
InvalidateRefreshTokens: true,
|
||||||
JWTSigningAlgorithm: "RS256",
|
JWTSigningAlgorithm: "RS256",
|
||||||
JWTSigningPrivateKeyFile: "jwt/private.pem",
|
JWTSigningPrivateKeyFile: "jwt/private.pem",
|
||||||
MaxTokenLength: math.MaxInt16,
|
MaxTokenLength: math.MaxInt16,
|
||||||
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"},
|
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"},
|
||||||
|
EnableAdditionalGrantScopes: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadOAuth2From(rootCfg ConfigProvider) {
|
func loadOAuth2From(rootCfg ConfigProvider) {
|
||||||
|
|
|
@ -110,5 +110,6 @@ func loadApplicationsData(ctx *context.Context) {
|
||||||
ctx.ServerError("GetOAuth2GrantsByUserID", err)
|
ctx.ServerError("GetOAuth2GrantsByUserID", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx.Data["EnableAdditionalGrantScopes"] = setting.OAuth2.EnableAdditionalGrantScopes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// check oauth2 token
|
// check oauth2 token
|
||||||
uid := CheckOAuthAccessToken(req.Context(), authToken)
|
uid, _ := CheckOAuthAccessToken(req.Context(), authToken)
|
||||||
if uid != 0 {
|
if uid != 0 {
|
||||||
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
|
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ package auth
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,28 +26,69 @@ var (
|
||||||
_ Method = &OAuth2{}
|
_ Method = &OAuth2{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// grantAdditionalScopes returns valid scopes coming from grant
|
||||||
|
func grantAdditionalScopes(grantScopes string) string {
|
||||||
|
// scopes_supported from templates/user/auth/oidc_wellknown.tmpl
|
||||||
|
scopesSupported := []string{
|
||||||
|
"openid",
|
||||||
|
"profile",
|
||||||
|
"email",
|
||||||
|
"groups",
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiTokenScopes []string
|
||||||
|
for _, apiTokenScope := range strings.Split(grantScopes, " ") {
|
||||||
|
if slices.Index(scopesSupported, apiTokenScope) == -1 {
|
||||||
|
apiTokenScopes = append(apiTokenScopes, apiTokenScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apiTokenScopes) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var additionalGrantScopes []string
|
||||||
|
allScopes := auth_model.AccessTokenScope("all")
|
||||||
|
|
||||||
|
for _, apiTokenScope := range apiTokenScopes {
|
||||||
|
grantScope := auth_model.AccessTokenScope(apiTokenScope)
|
||||||
|
if ok, _ := allScopes.HasScope(grantScope); ok {
|
||||||
|
additionalGrantScopes = append(additionalGrantScopes, apiTokenScope)
|
||||||
|
} else if apiTokenScope == "public-only" {
|
||||||
|
additionalGrantScopes = append(additionalGrantScopes, apiTokenScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(additionalGrantScopes) > 0 {
|
||||||
|
return strings.Join(additionalGrantScopes, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// CheckOAuthAccessToken returns uid of user from oauth token
|
// CheckOAuthAccessToken returns uid of user from oauth token
|
||||||
func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
|
// + non default openid scopes requested
|
||||||
|
func CheckOAuthAccessToken(ctx context.Context, accessToken string) (int64, string) {
|
||||||
// JWT tokens require a "."
|
// JWT tokens require a "."
|
||||||
if !strings.Contains(accessToken, ".") {
|
if !strings.Contains(accessToken, ".") {
|
||||||
return 0
|
return 0, ""
|
||||||
}
|
}
|
||||||
token, err := oauth2.ParseToken(accessToken, oauth2.DefaultSigningKey)
|
token, err := oauth2.ParseToken(accessToken, oauth2.DefaultSigningKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Trace("oauth2.ParseToken: %v", err)
|
log.Trace("oauth2.ParseToken: %v", err)
|
||||||
return 0
|
return 0, ""
|
||||||
}
|
}
|
||||||
var grant *auth_model.OAuth2Grant
|
var grant *auth_model.OAuth2Grant
|
||||||
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
|
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
|
||||||
return 0
|
return 0, ""
|
||||||
}
|
}
|
||||||
if token.Type != oauth2.TypeAccessToken {
|
if token.Type != oauth2.TypeAccessToken {
|
||||||
return 0
|
return 0, ""
|
||||||
}
|
}
|
||||||
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
|
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
|
||||||
return 0
|
return 0, ""
|
||||||
}
|
}
|
||||||
return grant.UserID
|
grantScopes := grantAdditionalScopes(grant.Scope)
|
||||||
|
return grant.UserID, grantScopes
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuth2 implements the Auth interface and authenticates requests
|
// OAuth2 implements the Auth interface and authenticates requests
|
||||||
|
@ -92,10 +134,15 @@ func parseToken(req *http.Request) (string, bool) {
|
||||||
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
|
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
|
||||||
// Let's see if token is valid.
|
// Let's see if token is valid.
|
||||||
if strings.Contains(tokenSHA, ".") {
|
if strings.Contains(tokenSHA, ".") {
|
||||||
uid := CheckOAuthAccessToken(ctx, tokenSHA)
|
uid, grantScopes := CheckOAuthAccessToken(ctx, tokenSHA)
|
||||||
|
|
||||||
if uid != 0 {
|
if uid != 0 {
|
||||||
store.GetData()["IsApiToken"] = true
|
store.GetData()["IsApiToken"] = true
|
||||||
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
|
if grantScopes != "" {
|
||||||
|
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScope(grantScopes)
|
||||||
|
} else {
|
||||||
|
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return uid
|
return uid
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue