mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-10 13:13:46 +00:00
50d2b26a28
# Which Problems Are Solved To be able to migrate or test the new login UI, admins might want to (temporarily) switch individual apps. At a later point admin might want to make sure all applications use the new login UI. # How the Problems Are Solved - Added a feature flag `` on instance level to require all apps to use the new login and provide an optional base url. - if the flag is enabled, all (OIDC) applications will automatically use the v2 login. - if disabled, applications can decide based on their configuration - Added an option on OIDC apps to use the new login UI and an optional base url. - Removed the requirement to use `x-zitadel-login-client` to be redirected to the login V2 and retrieve created authrequest and link them to SSO sessions. - Added a new "IAM_LOGIN_CLIENT" role to allow management of users, sessions, grants and more without `x-zitadel-login-client`. # Additional Changes None # Additional Context closes https://github.com/zitadel/zitadel/issues/8702
672 lines
25 KiB
Go
672 lines
25 KiB
Go
//go:build integration
|
|
|
|
package oidc_test
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/muhlemmer/gu"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
|
|
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
|
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
|
|
"github.com/zitadel/zitadel/internal/command"
|
|
"github.com/zitadel/zitadel/internal/integration"
|
|
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
|
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
|
)
|
|
|
|
var (
|
|
armPasskey = []string{oidc_api.UserPresence, oidc_api.MFA}
|
|
armPassword = []string{oidc_api.PWD}
|
|
)
|
|
|
|
func TestOPStorage_CreateAuthRequest(t *testing.T) {
|
|
clientID, _ := createClient(t, Instance)
|
|
clientIDV2, _ := createClientLoginV2(t, Instance)
|
|
|
|
id := createAuthRequest(t, Instance, clientID, redirectURI)
|
|
require.Contains(t, id, command.IDPrefixV2)
|
|
|
|
id2 := createAuthRequestNoLoginClientHeader(t, Instance, clientIDV2, redirectURI)
|
|
require.Contains(t, id2, command.IDPrefixV2)
|
|
}
|
|
|
|
func TestOPStorage_CreateAccessToken_code(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
clientID string
|
|
authRequestID func(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string
|
|
}{
|
|
{
|
|
name: "login header",
|
|
clientID: func() string {
|
|
clientID, _ := createClient(t, Instance)
|
|
return clientID
|
|
}(),
|
|
authRequestID: createAuthRequest,
|
|
},
|
|
{
|
|
name: "login v2 config",
|
|
clientID: func() string {
|
|
clientID, _ := createClientLoginV2(t, Instance)
|
|
return clientID
|
|
}(),
|
|
authRequestID: createAuthRequestNoLoginClientHeader,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
authRequestID := tt.authRequestID(t, Instance, tt.clientID, redirectURI)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// test code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, tt.clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, false)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// callback on a succeeded request must fail
|
|
linkResp, err = Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
|
|
// exchange with a used code must fail
|
|
_, err = exchangeTokens(t, Instance, tt.clientID, code, redirectURI)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOPStorage_CreateAccessToken_implicit(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
clientID string
|
|
authRequestID func(t testing.TB, clientID, redirectURI string, scope ...string) string
|
|
}{
|
|
{
|
|
name: "login header",
|
|
clientID: createImplicitClient(t),
|
|
authRequestID: createAuthRequestImplicit,
|
|
},
|
|
{
|
|
name: "login v2 config",
|
|
clientID: createImplicitClientNoLoginClientHeader(t),
|
|
authRequestID: createAuthRequestImplicitNoLoginClientHeader,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
authRequestID := tt.authRequestID(t, tt.clientID, redirectURIImplicit)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// test implicit callback
|
|
callback, err := url.Parse(linkResp.GetCallbackUrl())
|
|
require.NoError(t, err)
|
|
values, err := url.ParseQuery(callback.Fragment)
|
|
require.NoError(t, err)
|
|
accessToken := values.Get("access_token")
|
|
idToken := values.Get("id_token")
|
|
refreshToken := values.Get("refresh_token")
|
|
assert.NotEmpty(t, accessToken)
|
|
assert.NotEmpty(t, idToken)
|
|
assert.Empty(t, refreshToken)
|
|
assert.NotEmpty(t, values.Get("expires_in"))
|
|
assert.Equal(t, oidc.BearerToken, values.Get("token_type"))
|
|
assert.Equal(t, "state", values.Get("state"))
|
|
|
|
// check id_token / claims
|
|
provider, err := Instance.CreateRelyingParty(CTX, tt.clientID, redirectURIImplicit)
|
|
require.NoError(t, err)
|
|
claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier())
|
|
require.NoError(t, err)
|
|
assertIDTokenClaims(t, claims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// callback on a succeeded request must fail
|
|
linkResp, err = Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
clientID string
|
|
authRequestID func(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string
|
|
}{
|
|
{
|
|
name: "login header",
|
|
clientID: func() string {
|
|
clientID, _ := createClient(t, Instance)
|
|
return clientID
|
|
}(),
|
|
authRequestID: createAuthRequest,
|
|
},
|
|
{
|
|
name: "login v2 config",
|
|
clientID: func() string {
|
|
clientID, _ := createClientLoginV2(t, Instance)
|
|
return clientID
|
|
}(),
|
|
authRequestID: createAuthRequestNoLoginClientHeader,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
authRequestID := tt.authRequestID(t, Instance, tt.clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// test code exchange (expect refresh token to be returned)
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, tt.clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, true)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
clientID string
|
|
authRequestID func(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string
|
|
}{
|
|
{
|
|
name: "login header",
|
|
clientID: func() string {
|
|
clientID, _ := createClient(t, Instance)
|
|
return clientID
|
|
}(),
|
|
authRequestID: createAuthRequest,
|
|
},
|
|
{
|
|
name: "login v2 config",
|
|
clientID: func() string {
|
|
clientID, _ := createClientLoginV2(t, Instance)
|
|
return clientID
|
|
}(),
|
|
authRequestID: createAuthRequestNoLoginClientHeader,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
provider, err := Instance.CreateRelyingParty(CTX, tt.clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
authRequestID := tt.authRequestID(t, Instance, tt.clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, tt.clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, true)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// test actual refresh grant
|
|
newTokens, err := refreshTokens(t, tt.clientID, tokens.RefreshToken)
|
|
require.NoError(t, err)
|
|
assertTokens(t, newTokens, true)
|
|
// auth time must still be the initial
|
|
assertIDTokenClaims(t, newTokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// refresh with an old refresh_token must fail
|
|
_, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "")
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOPStorage_RevokeToken_access_token(t *testing.T) {
|
|
clientID, _ := createClient(t, Instance)
|
|
provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
authRequestID := createAuthRequest(t, Instance, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, true)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// revoke access token
|
|
err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "access_token")
|
|
require.NoError(t, err)
|
|
|
|
// userinfo must fail
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
require.Error(t, err)
|
|
|
|
// refresh grant must still work
|
|
_, err = refreshTokens(t, clientID, tokens.RefreshToken)
|
|
require.NoError(t, err)
|
|
|
|
// revocation with the same access token must not fail (with or without hint)
|
|
err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "access_token")
|
|
require.NoError(t, err)
|
|
err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T) {
|
|
clientID, _ := createClient(t, Instance)
|
|
provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
authRequestID := createAuthRequest(t, Instance, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, true)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// revoke access token
|
|
err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "refresh_token")
|
|
require.NoError(t, err)
|
|
|
|
// userinfo must fail
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
require.Error(t, err)
|
|
|
|
// refresh grant must still work
|
|
_, err = refreshTokens(t, clientID, tokens.RefreshToken)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestOPStorage_RevokeToken_refresh_token(t *testing.T) {
|
|
clientID, _ := createClient(t, Instance)
|
|
provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
authRequestID := createAuthRequest(t, Instance, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, true)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// revoke refresh token -> invalidates also access token
|
|
err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "refresh_token")
|
|
require.NoError(t, err)
|
|
|
|
// userinfo must fail
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
require.Error(t, err)
|
|
|
|
// refresh must fail
|
|
_, err = refreshTokens(t, clientID, tokens.RefreshToken)
|
|
require.Error(t, err)
|
|
|
|
// revocation with the same refresh token must not fail (with or without hint)
|
|
err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "refresh_token")
|
|
require.NoError(t, err)
|
|
err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing.T) {
|
|
clientID, _ := createClient(t, Instance)
|
|
provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
authRequestID := createAuthRequest(t, Instance, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, true)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// revoke refresh token even with a wrong hint
|
|
err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "access_token")
|
|
require.NoError(t, err)
|
|
|
|
// userinfo must fail
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
require.Error(t, err)
|
|
|
|
// refresh must fail
|
|
_, err = refreshTokens(t, clientID, tokens.RefreshToken)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestOPStorage_RevokeToken_invalid_client(t *testing.T) {
|
|
clientID, _ := createClient(t, Instance)
|
|
authRequestID := createAuthRequest(t, Instance, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, true)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// simulate second client (not part of the audience) trying to revoke the token
|
|
otherClientID, _ := createClient(t, Instance)
|
|
provider, err := Instance.CreateRelyingParty(CTX, otherClientID, redirectURI)
|
|
require.NoError(t, err)
|
|
err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestOPStorage_TerminateSession(t *testing.T) {
|
|
clientID, _ := createClient(t, Instance)
|
|
provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
authRequestID := createAuthRequest(t, Instance, clientID, redirectURI)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// test code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, false)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// userinfo must not fail
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
require.NoError(t, err)
|
|
|
|
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())
|
|
|
|
// userinfo must fail
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
|
|
clientID, _ := createClient(t, Instance)
|
|
provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
authRequestID := createAuthRequest(t, Instance, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// test code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, true)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
// userinfo must not fail
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
require.NoError(t, err)
|
|
|
|
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())
|
|
|
|
// userinfo must fail
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
require.Error(t, err)
|
|
|
|
refreshedTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
|
|
require.NoError(t, err)
|
|
|
|
// userinfo must not fail
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, refreshedTokens.AccessToken, refreshedTokens.TokenType, refreshedTokens.IDTokenClaims.Subject, provider)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
clientID string
|
|
authRequestID func(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string
|
|
logoutURL string
|
|
}{
|
|
{
|
|
name: "login header",
|
|
clientID: func() string {
|
|
clientID, _ := createClient(t, Instance)
|
|
return clientID
|
|
}(),
|
|
authRequestID: createAuthRequest,
|
|
logoutURL: http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure) + Instance.Config.LogoutURLV2 + logoutRedirectURI + "?state=state",
|
|
},
|
|
{
|
|
name: "login v2 config",
|
|
clientID: func() string {
|
|
clientID, _ := createClientLoginV2(t, Instance)
|
|
return clientID
|
|
}(),
|
|
authRequestID: createAuthRequestNoLoginClientHeader,
|
|
logoutURL: http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure) + Instance.Config.LogoutURLV2 + logoutRedirectURI + "?state=state",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
provider, err := Instance.CreateRelyingParty(CTX, tt.clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
authRequestID := tt.authRequestID(t, Instance, tt.clientID, redirectURI)
|
|
sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
|
linkResp, err := Instance.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
|
AuthRequestId: authRequestID,
|
|
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
|
Session: &oidc_pb.Session{
|
|
SessionId: sessionID,
|
|
SessionToken: sessionToken,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// test code exchange
|
|
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
|
tokens, err := exchangeTokens(t, Instance, tt.clientID, code, redirectURI)
|
|
require.NoError(t, err)
|
|
assertTokens(t, tokens, false)
|
|
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
|
|
|
|
postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.logoutURL, postLogoutRedirect.String())
|
|
|
|
// userinfo must not fail until login UI terminated session
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
require.NoError(t, err)
|
|
|
|
// simulate termination by login UI
|
|
_, err = Instance.Client.SessionV2.DeleteSession(CTXLOGIN, &session.DeleteSessionRequest{
|
|
SessionId: sessionID,
|
|
SessionToken: gu.Ptr(sessionToken),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// userinfo must fail
|
|
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func exchangeTokens(t testing.TB, instance *integration.Instance, clientID, code, redirectURI string) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
|
|
provider, err := instance.CreateRelyingParty(CTX, clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
|
|
return rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, rp.WithCodeVerifier(integration.CodeVerifier))
|
|
}
|
|
|
|
func refreshTokens(t testing.TB, clientID, refreshToken string) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
|
|
provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI)
|
|
require.NoError(t, err)
|
|
|
|
return rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, refreshToken, "", "")
|
|
}
|
|
|
|
func assertCodeResponse(t *testing.T, callback string) string {
|
|
callbackURL, err := url.Parse(callback)
|
|
require.NoError(t, err)
|
|
code := callbackURL.Query().Get("code")
|
|
require.NotEmpty(t, code)
|
|
assert.Equal(t, "state", callbackURL.Query().Get("state"))
|
|
return code
|
|
}
|
|
|
|
func assertTokens(t *testing.T, tokens *oidc.Tokens[*oidc.IDTokenClaims], requireRefreshToken bool) {
|
|
assert.NotEmpty(t, tokens.AccessToken)
|
|
assert.NotEmpty(t, tokens.IDToken)
|
|
if requireRefreshToken {
|
|
assert.NotEmpty(t, tokens.RefreshToken)
|
|
} else {
|
|
assert.Empty(t, tokens.RefreshToken)
|
|
}
|
|
// since we test implicit flow directly, we can check that any token response must not
|
|
// return a `state` in the response
|
|
assert.Empty(t, tokens.Extra("state"))
|
|
}
|
|
|
|
func assertIDTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, userID string, arm []string, sessionStart, sessionChange time.Time, sessionID string) {
|
|
assert.Equal(t, userID, claims.Subject)
|
|
assert.Equal(t, arm, claims.AuthenticationMethodsReferences)
|
|
assertOIDCTimeRange(t, claims.AuthTime, sessionStart, sessionChange)
|
|
assert.Equal(t, sessionID, claims.SessionID)
|
|
assert.Empty(t, claims.Name)
|
|
assert.Empty(t, claims.GivenName)
|
|
assert.Empty(t, claims.FamilyName)
|
|
assert.Empty(t, claims.PreferredUsername)
|
|
}
|