mirror of
https://github.com/fatedier/frp.git
synced 2025-02-15 09:50:35 +01:00
Implement OIDC raw token and hd claim verification
This commit is contained in:
parent
450b8393bc
commit
cda2cb151e
@ -51,7 +51,7 @@ func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
|
||||
authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
||||
case v1.AuthMethodOIDC:
|
||||
tokenVerifier := NewTokenVerifier(cfg.OIDC)
|
||||
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier)
|
||||
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier, cfg.OIDC.AllowedHostedDomains)
|
||||
}
|
||||
return authVerifier
|
||||
}
|
||||
|
@ -16,8 +16,11 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
@ -30,6 +33,10 @@ type OidcAuthProvider struct {
|
||||
additionalAuthScopes []v1.AuthScope
|
||||
|
||||
tokenGenerator *clientcredentials.Config
|
||||
|
||||
// rawToken is used to specify a raw JWT token for authentication.
|
||||
// If rawToken is not empty, it will be used directly instead of generating a new token.
|
||||
rawToken string
|
||||
}
|
||||
|
||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider {
|
||||
@ -53,10 +60,17 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
|
||||
return &OidcAuthProvider{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
tokenGenerator: tokenGenerator,
|
||||
rawToken: cfg.RawToken,
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||
// If a raw token is provided, use it directly.
|
||||
if auth.rawToken != "" {
|
||||
return auth.rawToken, nil
|
||||
}
|
||||
|
||||
// Otherwise, generate a new token using the client credentials flow.
|
||||
tokenObj, err := auth.tokenGenerator.Token(context.Background())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
|
||||
@ -96,6 +110,9 @@ type OidcAuthConsumer struct {
|
||||
|
||||
verifier TokenVerifier
|
||||
subjectsFromLogin []string
|
||||
|
||||
// allowedHostedDomains specifies a list of allowed hosted domains for the "hd" claim in the token.
|
||||
allowedHostedDomains []string
|
||||
}
|
||||
|
||||
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
||||
@ -112,15 +129,52 @@ func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
||||
return provider.Verifier(&verifierConf)
|
||||
}
|
||||
|
||||
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier) *OidcAuthConsumer {
|
||||
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier, allowedHostedDomains []string) *OidcAuthConsumer {
|
||||
return &OidcAuthConsumer{
|
||||
additionalAuthScopes: additionalAuthScopes,
|
||||
verifier: verifier,
|
||||
subjectsFromLogin: []string{},
|
||||
allowedHostedDomains: allowedHostedDomains,
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) {
|
||||
// Decode token without verifying signature to retrieved 'hd' claim.
|
||||
parts := strings.Split(loginMsg.PrivilegeKey, ".")
|
||||
if len(parts) != 3 {
|
||||
return fmt.Errorf("invalid OIDC token format")
|
||||
}
|
||||
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid OIDC token: failed to decode payload: %v", err)
|
||||
}
|
||||
|
||||
var claims map[string]any
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return fmt.Errorf("invalid OIDC token: failed to unmarshal payload: %v", err)
|
||||
}
|
||||
|
||||
// Verify hosted domain (hd claim).
|
||||
if len(auth.allowedHostedDomains) > 0 {
|
||||
hd, ok := claims["hd"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("OIDC token missing required 'hd' claim")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, domain := range auth.allowedHostedDomains {
|
||||
if hd == domain {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("OIDC token 'hd' claim [%s] is not in allowed list", hd)
|
||||
}
|
||||
}
|
||||
|
||||
// If hd check passes, proceed with standard verification.
|
||||
token, err := auth.verifier.Verify(context.Background(), loginMsg.PrivilegeKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid OIDC token in login: %v", err)
|
||||
|
@ -23,7 +23,7 @@ func (m *mockTokenVerifier) Verify(ctx context.Context, subject string) (*oidc.I
|
||||
|
||||
func TestPingWithEmptySubjectFromLoginFails(t *testing.T) {
|
||||
r := require.New(t)
|
||||
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
|
||||
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}, []string{})
|
||||
err := consumer.VerifyPing(&msg.Ping{
|
||||
PrivilegeKey: "ping-without-login",
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
@ -34,7 +34,7 @@ func TestPingWithEmptySubjectFromLoginFails(t *testing.T) {
|
||||
|
||||
func TestPingAfterLoginWithNewSubjectSucceeds(t *testing.T) {
|
||||
r := require.New(t)
|
||||
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
|
||||
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}, []string{})
|
||||
err := consumer.VerifyLogin(&msg.Login{
|
||||
PrivilegeKey: "ping-after-login",
|
||||
})
|
||||
@ -49,7 +49,7 @@ func TestPingAfterLoginWithNewSubjectSucceeds(t *testing.T) {
|
||||
|
||||
func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) {
|
||||
r := require.New(t)
|
||||
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
|
||||
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}, []string{})
|
||||
err := consumer.VerifyLogin(&msg.Login{
|
||||
PrivilegeKey: "login-with-first-subject",
|
||||
})
|
||||
|
@ -203,4 +203,7 @@ type AuthOIDCClientConfig struct {
|
||||
// AdditionalEndpointParams specifies additional parameters to be sent
|
||||
// this field will be transfer to map[string][]string in OIDC token generator.
|
||||
AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"`
|
||||
// RawToken specifies a raw JWT token to use for authentication, bypassing
|
||||
// the OIDC flow.
|
||||
RawToken string `json:"rawToken,omitempty"`
|
||||
}
|
||||
|
@ -147,6 +147,16 @@ type AuthOIDCServerConfig struct {
|
||||
// SkipIssuerCheck specifies whether to skip checking if the OIDC token's
|
||||
// issuer claim matches the issuer specified in OidcIssuer.
|
||||
SkipIssuerCheck bool `json:"skipIssuerCheck,omitempty"`
|
||||
// AllowedHostedDomains specifies a list of allowed hosted domains for the
|
||||
// "hd" claim in the token.
|
||||
AllowedHostedDomains []string `json:"allowedHostedDomains,omitempty"`
|
||||
}
|
||||
|
||||
func (c *AuthOIDCServerConfig) Complete() {
|
||||
// Ensure AllowedHostedDomains is an empty slice and not nil
|
||||
if c.AllowedHostedDomains == nil {
|
||||
c.AllowedHostedDomains = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
type ServerTransportConfig struct {
|
||||
|
Loading…
Reference in New Issue
Block a user