mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 18:17:35 +00:00
feat: JWT IdP intent (#9966)
# Which Problems Are Solved The login v1 allowed to use JWTs as IdP using the JWT IDP. The login V2 uses idp intents for such cases, which were not yet able to handle JWT IdPs. # How the Problems Are Solved - Added handling of JWT IdPs in `StartIdPIntent` and `RetrieveIdPIntent` - The redirect returned by the start, uses the existing `authRequestID` and `userAgentID` parameter names for compatibility reasons. - Added `/idps/jwt` endpoint to handle the proxied (callback) endpoint , which extracts and validates the JWT against the configured endpoint. # Additional Changes None # Additional Context - closes #9758
This commit is contained in:
@@ -11,14 +11,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
queryAuthRequestID = "authRequestID"
|
||||
queryUserAgentID = "userAgentID"
|
||||
QueryAuthRequestID = "authRequestID"
|
||||
QueryUserAgentID = "userAgentID"
|
||||
)
|
||||
|
||||
var _ idp.Provider = (*Provider)(nil)
|
||||
|
||||
var (
|
||||
ErrMissingUserAgentID = errors.New("userAgentID missing")
|
||||
ErrMissingState = errors.New("state missing")
|
||||
)
|
||||
|
||||
// Provider is the [idp.Provider] implementation for a JWT provider
|
||||
@@ -92,32 +92,32 @@ func (p *Provider) Name() string {
|
||||
// It will create a [Session] with an AuthURL, pointing to the jwtEndpoint
|
||||
// with the authRequest and encrypted userAgent ids.
|
||||
func (p *Provider) BeginAuth(ctx context.Context, state string, params ...idp.Parameter) (idp.Session, error) {
|
||||
userAgentID, err := userAgentIDFromParams(params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if state == "" {
|
||||
return nil, ErrMissingState
|
||||
}
|
||||
userAgentID := userAgentIDFromParams(state, params...)
|
||||
redirect, err := url.Parse(p.jwtEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := redirect.Query()
|
||||
q.Set(queryAuthRequestID, state)
|
||||
q.Set(QueryAuthRequestID, state)
|
||||
nonce, err := p.encryptionAlg.Encrypt([]byte(userAgentID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q.Set(queryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce))
|
||||
q.Set(QueryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce))
|
||||
redirect.RawQuery = q.Encode()
|
||||
return &Session{AuthURL: redirect.String()}, nil
|
||||
}
|
||||
|
||||
func userAgentIDFromParams(params ...idp.Parameter) (string, error) {
|
||||
func userAgentIDFromParams(state string, params ...idp.Parameter) string {
|
||||
for _, param := range params {
|
||||
if id, ok := param.(idp.UserAgentID); ok {
|
||||
return string(id), nil
|
||||
return string(id)
|
||||
}
|
||||
}
|
||||
return "", ErrMissingUserAgentID
|
||||
return state
|
||||
}
|
||||
|
||||
// IsLinkingAllowed implements the [idp.Provider] interface.
|
||||
|
@@ -23,6 +23,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
encryptionAlg func(t *testing.T) crypto.EncryptionAlgorithm
|
||||
}
|
||||
type args struct {
|
||||
state string
|
||||
params []idp.Parameter
|
||||
}
|
||||
type want struct {
|
||||
@@ -36,7 +37,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "missing userAgentID error",
|
||||
name: "missing state, error",
|
||||
fields: fields{
|
||||
issuer: "https://jwt.com",
|
||||
jwtEndpoint: "https://auth.com/jwt",
|
||||
@@ -47,14 +48,34 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
state: "",
|
||||
params: nil,
|
||||
},
|
||||
want: want{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, ErrMissingUserAgentID)
|
||||
return errors.Is(err, ErrMissingState)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing userAgentID, fallback to state",
|
||||
fields: fields{
|
||||
issuer: "https://jwt.com",
|
||||
jwtEndpoint: "https://auth.com/jwt",
|
||||
keysEndpoint: "https://jwt.com/keys",
|
||||
headerName: "jwt-header",
|
||||
encryptionAlg: func(t *testing.T) crypto.EncryptionAlgorithm {
|
||||
return crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
state: "testState",
|
||||
params: nil,
|
||||
},
|
||||
want: want{
|
||||
session: &Session{AuthURL: "https://auth.com/jwt?authRequestID=testState&userAgentID=dGVzdFN0YXRl"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful auth",
|
||||
fields: fields{
|
||||
@@ -67,6 +88,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
state: "testState",
|
||||
params: []idp.Parameter{
|
||||
idp.UserAgentID("agent"),
|
||||
},
|
||||
@@ -91,7 +113,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
session, err := provider.BeginAuth(ctx, "testState", tt.args.params...)
|
||||
session, err := provider.BeginAuth(ctx, tt.args.state, tt.args.params...)
|
||||
if tt.want.err != nil && !tt.want.err(err) {
|
||||
a.Fail("invalid error", err)
|
||||
}
|
||||
|
@@ -5,11 +5,13 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@@ -34,6 +36,11 @@ func NewSession(provider *Provider, tokens *oidc.Tokens[*oidc.IDTokenClaims]) *S
|
||||
return &Session{Provider: provider, Tokens: tokens}
|
||||
}
|
||||
|
||||
func NewSessionFromRequest(provider *Provider, r *http.Request) *Session {
|
||||
token := strings.TrimPrefix(r.Header.Get(provider.headerName), oidc.PrefixBearer)
|
||||
return NewSession(provider, &oidc.Tokens[*oidc.IDTokenClaims]{IDToken: token, Token: &oauth2.Token{}})
|
||||
}
|
||||
|
||||
// GetAuth implements the [idp.Session] interface.
|
||||
func (s *Session) GetAuth(ctx context.Context) (string, bool) {
|
||||
return idp.Redirect(s.AuthURL)
|
||||
@@ -99,6 +106,12 @@ func (s *Session) validateToken(ctx context.Context, token string) (*oidc.IDToke
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func InitUser() *User {
|
||||
return &User{
|
||||
IDTokenClaims: &oidc.IDTokenClaims{},
|
||||
}
|
||||
}
|
||||
|
||||
type User struct {
|
||||
*oidc.IDTokenClaims
|
||||
}
|
||||
|
Reference in New Issue
Block a user