feat(API): support V2 token and session token usage (#6180)

This PR adds support for userinfo and introspection of V2 tokens. Further V2 access tokens and session tokens can be used for authentication on the ZITADEL API (like the current access tokens).
This commit is contained in:
Livio Spring
2023-07-14 13:16:16 +02:00
committed by GitHub
parent 4589ddad4a
commit 80961125a7
38 changed files with 1309 additions and 181 deletions

View File

@@ -5,7 +5,6 @@ package oidc_test
import (
"context"
"net/url"
"os"
"testing"
"time"
@@ -17,63 +16,14 @@ import (
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/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
var (
CTX context.Context
CTXLOGIN context.Context
Tester *integration.Tester
User *user.AddHumanUserResponse
armPasskey = []string{oidc_api.UserPresence, oidc_api.MFA}
armPassword = []string{oidc_api.PWD}
)
const (
redirectURI = "oidcIntegrationTest://callback"
redirectURIImplicit = "http://localhost:9999/callback"
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
User = Tester.CreateHumanUser(CTX)
Tester.RegisterUserPasskey(CTX, User.GetUserId())
CTXLOGIN, _ = Tester.WithAuthorization(ctx, integration.Login), errCtx
return m.Run()
}())
}
func createClient(t testing.TB) string {
app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI)
require.NoError(t, err)
return app.GetClientId()
}
func createImplicitClient(t testing.TB) string {
app, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
require.NoError(t, err)
return app.GetClientId()
}
func createAuthRequest(t testing.TB, clientID, redirectURI string, scope ...string) string {
redURL, err := Tester.CreateOIDCAuthRequest(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
require.NoError(t, err)
return redURL
}
func createAuthRequestImplicit(t testing.TB, clientID, redirectURI string, scope ...string) string {
redURL, err := Tester.CreateOIDCAuthRequestImplicit(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
require.NoError(t, err)
return redURL
}
func TestOPStorage_CreateAuthRequest(t *testing.T) {
clientID := createClient(t)
@@ -101,7 +51,7 @@ func TestOPStorage_CreateAccessToken_code(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, false)
assertTokenClaims(t, tokens.IDTokenClaims, startTime, changeTime)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
// callback on a succeeded request must fail
linkResp, err = Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
@@ -155,7 +105,7 @@ func TestOPStorage_CreateAccessToken_implicit(t *testing.T) {
require.NoError(t, err)
claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier())
require.NoError(t, err)
assertTokenClaims(t, claims, startTime, changeTime)
assertIDTokenClaims(t, claims, armPasskey, startTime, changeTime)
// callback on a succeeded request must fail
linkResp, err = Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
@@ -190,7 +140,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, true)
assertTokenClaims(t, tokens.IDTokenClaims, startTime, changeTime)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
}
func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
@@ -215,7 +165,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, true)
assertTokenClaims(t, tokens.IDTokenClaims, startTime, changeTime)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
// test actual refresh grant
newTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
@@ -227,7 +177,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), newTokens.AccessToken, idToken, provider.IDTokenVerifier())
require.NoError(t, err)
// auth time must still be the initial
assertTokenClaims(t, claims, startTime, changeTime)
assertIDTokenClaims(t, claims, armPasskey, startTime, changeTime)
// refresh with an old refresh_token must fail
_, err = rp.RefreshAccessToken(provider, tokens.RefreshToken, "", "")
@@ -268,8 +218,8 @@ func assertTokens(t *testing.T, tokens *oidc.Tokens[*oidc.IDTokenClaims], requir
}
}
func assertTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, sessionStart, sessionChange time.Time) {
func assertIDTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, arm []string, sessionStart, sessionChange time.Time) {
assert.Equal(t, User.GetUserId(), claims.Subject)
assert.Equal(t, []string{oidc_api.UserPresence, oidc_api.MFA}, claims.AuthenticationMethodsReferences)
assert.WithinRange(t, claims.AuthTime.AsTime().UTC(), sessionStart.Add(-1*time.Second), sessionChange.Add(1*time.Second))
assert.Equal(t, arm, claims.AuthenticationMethodsReferences)
assertOIDCTimeRange(t, claims.AuthTime, sessionStart, sessionChange)
}

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/dop251/goja"
"github.com/zitadel/logging"
@@ -17,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/actions/object"
"github.com/zitadel/zitadel/internal/api/authz"
api_http "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
@@ -119,18 +121,26 @@ func (o *OPStorage) AuthorizeClientIDSecret(ctx context.Context, id string, secr
func (o *OPStorage) SetUserinfoFromToken(ctx context.Context, userInfo *oidc.UserInfo, tokenID, subject, origin string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
token, err := o.query.ActiveAccessTokenByToken(ctx, tokenID)
if err != nil {
return err
}
if err = o.isOriginAllowed(ctx, token.ClientID, origin); err != nil {
return err
}
return o.setUserinfo(ctx, userInfo, token.UserID, token.ClientID, token.Scope, nil)
}
token, err := o.repo.TokenByIDs(ctx, subject, tokenID)
if err != nil {
return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
}
if token.ApplicationID != "" {
app, err := o.query.AppByOIDCClientID(ctx, token.ApplicationID, false)
if err != nil {
if err = o.isOriginAllowed(ctx, token.ApplicationID, origin); err != nil {
return err
}
if origin != "" && !api_http.IsOriginAllowed(app.OIDCConfig.AllowedOrigins, origin) {
return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
}
}
return o.setUserinfo(ctx, userInfo, token.UserID, token.ApplicationID, token.Scopes, nil)
}
@@ -154,6 +164,24 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo *oidc.Us
}
func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
token, err := o.query.ActiveAccessTokenByToken(ctx, tokenID)
if err != nil {
return err
}
projectID, err := o.query.ProjectIDFromClientID(ctx, clientID, false)
if err != nil {
return errors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found")
}
return o.introspect(ctx, introspection,
tokenID, token.UserID, token.ClientID, projectID,
token.Audience, token.Scope,
token.AccessTokenCreation, token.AccessTokenExpiration)
}
token, err := o.repo.TokenByIDs(ctx, subject, tokenID)
if err != nil {
return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
@@ -168,27 +196,10 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
return errors.ThrowPreconditionFailed(err, "OIDC-AGefw", "Errors.Internal")
}
}
for _, aud := range token.Audience {
if aud == clientID || aud == projectID {
userInfo := new(oidc.UserInfo)
err := o.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes, []string{projectID}) // always
if err != nil {
return err
}
introspection.SetUserInfo(userInfo)
introspection.Scope = token.Scopes
introspection.ClientID = token.ApplicationID
introspection.TokenType = oidc.BearerToken
introspection.Expiration = oidc.FromTime(token.Expiration)
introspection.IssuedAt = oidc.FromTime(token.CreationDate)
introspection.NotBefore = oidc.FromTime(token.CreationDate)
introspection.Audience = token.Audience
introspection.Issuer = op.IssuerFromContext(ctx)
introspection.JWTID = token.ID
return nil
}
}
return errors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
return o.introspect(ctx, introspection,
token.ID, token.UserID, token.ApplicationID, projectID,
token.Audience, token.Scopes,
token.CreationDate, token.Expiration)
}
func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scope []string) (op.TokenRequest, error) {
@@ -230,6 +241,55 @@ func (o *OPStorage) ClientCredentials(ctx context.Context, clientID, clientSecre
}, nil
}
// isOriginAllowed checks whether a call by the client to the endpoint is allowed from the provided origin
// if no origin is provided, no error will be returned
func (o *OPStorage) isOriginAllowed(ctx context.Context, clientID, origin string) error {
if origin == "" {
return nil
}
app, err := o.query.AppByOIDCClientID(ctx, clientID, false)
if err != nil {
return err
}
if api_http.IsOriginAllowed(app.OIDCConfig.AllowedOrigins, origin) {
return nil
}
return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
}
func (o *OPStorage) introspect(
ctx context.Context,
introspection *oidc.IntrospectionResponse,
tokenID, subject, clientID, projectID string,
audience, scope []string,
tokenCreation, tokenExpiration time.Time,
) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
for _, aud := range audience {
if aud == clientID || aud == projectID {
userInfo := new(oidc.UserInfo)
err = o.setUserinfo(ctx, userInfo, subject, clientID, scope, []string{projectID}) // always
if err != nil {
return err
}
introspection.SetUserInfo(userInfo)
introspection.Scope = scope
introspection.ClientID = clientID
introspection.TokenType = oidc.BearerToken
introspection.Expiration = oidc.FromTime(tokenExpiration)
introspection.IssuedAt = oidc.FromTime(tokenCreation)
introspection.NotBefore = oidc.FromTime(tokenCreation)
introspection.Audience = audience
introspection.Issuer = op.IssuerFromContext(ctx)
introspection.JWTID = tokenID
return nil
}
}
return errors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
}
func (o *OPStorage) checkOrgScopes(ctx context.Context, user *query.User, scopes []string) ([]string, error) {
for i := len(scopes) - 1; i >= 0; i-- {
scope := scopes[i]

View File

@@ -0,0 +1,135 @@
//go:build integration
package oidc_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v2/pkg/client/rp"
"github.com/zitadel/oidc/v2/pkg/client/rs"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/zitadel/pkg/grpc/authn"
"github.com/zitadel/zitadel/pkg/grpc/management"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
)
func TestOPStorage_SetUserinfoFromToken(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.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, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, true)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
// test actual userinfo
provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
require.NoError(t, err)
userinfo, err := rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
require.NoError(t, err)
assertUserinfo(t, userinfo)
}
func TestOPStorage_SetIntrospectionFromToken(t *testing.T) {
project, err := Tester.CreateProject(CTX)
require.NoError(t, err)
app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, project.GetId())
require.NoError(t, err)
api, err := Tester.CreateAPIClient(CTX, project.GetId())
require.NoError(t, err)
keyResp, err := Tester.Client.Mgmt.AddAppKey(CTX, &management.AddAppKeyRequest{
ProjectId: project.GetId(),
AppId: api.GetAppId(),
Type: authn.KeyType_KEY_TYPE_JSON,
ExpirationDate: nil,
})
require.NoError(t, err)
resourceServer, err := Tester.CreateResourceServer(keyResp.GetKeyDetails())
require.NoError(t, err)
scope := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess}
authRequestID := createAuthRequest(t, app.GetClientId(), redirectURI, scope...)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.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, app.GetClientId(), code)
require.NoError(t, err)
assertTokens(t, tokens, true)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
// test actual introspection
introspection, err := rs.Introspect(context.Background(), resourceServer, tokens.AccessToken)
require.NoError(t, err)
assertIntrospection(t, introspection,
Tester.OIDCIssuer(), app.GetClientId(),
scope, []string{app.GetClientId(), api.GetClientId(), project.GetId()},
tokens.Expiry, tokens.Expiry.Add(-12*time.Hour))
}
func assertUserinfo(t *testing.T, userinfo *oidc.UserInfo) {
assert.Equal(t, User.GetUserId(), userinfo.Subject)
assert.Equal(t, "Mickey", userinfo.GivenName)
assert.Equal(t, "Mouse", userinfo.FamilyName)
assert.Equal(t, "Mickey Mouse", userinfo.Name)
assert.NotEmpty(t, userinfo.PreferredUsername)
assert.Equal(t, userinfo.PreferredUsername, userinfo.Email)
assert.False(t, bool(userinfo.EmailVerified))
assertOIDCTime(t, userinfo.UpdatedAt, User.GetDetails().GetChangeDate().AsTime())
}
func assertIntrospection(
t *testing.T,
introspection *oidc.IntrospectionResponse,
issuer, clientID string,
scope, audience []string,
expiration, creation time.Time,
) {
assert.True(t, introspection.Active)
assert.Equal(t, scope, []string(introspection.Scope))
assert.Equal(t, clientID, introspection.ClientID)
assert.Equal(t, oidc.BearerToken, introspection.TokenType)
assertOIDCTime(t, introspection.Expiration, expiration)
assertOIDCTime(t, introspection.IssuedAt, creation)
assertOIDCTime(t, introspection.NotBefore, creation)
assert.Equal(t, User.GetUserId(), introspection.Subject)
assert.ElementsMatch(t, audience, introspection.Audience)
assert.Equal(t, issuer, introspection.Issuer)
assert.NotEmpty(t, introspection.JWTID)
assert.NotEmpty(t, introspection.Username)
assert.Equal(t, introspection.Username, introspection.PreferredUsername)
assert.Equal(t, "Mickey", introspection.GivenName)
assert.Equal(t, "Mouse", introspection.FamilyName)
assert.Equal(t, "Mickey Mouse", introspection.Name)
assert.Equal(t, introspection.Username, introspection.Email)
assert.False(t, bool(introspection.EmailVerified))
assertOIDCTime(t, introspection.UpdatedAt, User.GetDetails().GetChangeDate().AsTime())
}

View File

@@ -0,0 +1,248 @@
//go:build integration
package oidc_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v2/pkg/oidc"
"google.golang.org/grpc/metadata"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/auth"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
var (
CTX context.Context
CTXLOGIN context.Context
Tester *integration.Tester
User *user.AddHumanUserResponse
)
const (
redirectURI = "oidcIntegrationTest://callback"
redirectURIImplicit = "http://localhost:9999/callback"
zitadelAudienceScope = domain.ProjectIDScope + domain.ProjectIDScopeZITADEL + domain.AudSuffix
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
User = Tester.CreateHumanUser(CTX)
Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword)
Tester.RegisterUserPasskey(CTX, User.GetUserId())
CTXLOGIN, _ = Tester.WithAuthorization(ctx, integration.Login), errCtx
return m.Run()
}())
}
func Test_ZITADEL_API_missing_audience_scope(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.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, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, false)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.Error(t, err)
require.Nil(t, myUserResp)
}
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
createResp, err := Tester.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{UserId: User.GetUserId()},
},
},
Domain: Tester.Config.ExternalDomain,
})
require.NoError(t, err)
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: createResp.GetSessionId(),
SessionToken: createResp.GetSessionToken(),
},
},
})
require.NoError(t, err)
// code exchange
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.Error(t, err)
require.Nil(t, myUserResp)
}
func Test_ZITADEL_API_missing_mfa(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
linkResp, err := Tester.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, clientID, code)
require.NoError(t, err)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPassword, startTime, changeTime)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.Error(t, err)
require.Nil(t, myUserResp)
}
func Test_ZITADEL_API_success(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.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, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, false)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.NoError(t, err)
require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
}
func Test_ZITADEL_API_inactive_access_token(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.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, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, true)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
// make sure token works
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.NoError(t, err)
require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
// refresh token
newTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
require.NoError(t, err)
assert.NotEmpty(t, newTokens.AccessToken)
// use invalidated token
ctx = metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err = Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.Error(t, err)
require.Nil(t, myUserResp)
}
func createClient(t testing.TB) string {
project, err := Tester.CreateProject(CTX)
require.NoError(t, err)
app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, project.GetId())
require.NoError(t, err)
return app.GetClientId()
}
func createImplicitClient(t testing.TB) string {
app, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
require.NoError(t, err)
return app.GetClientId()
}
func createAuthRequest(t testing.TB, clientID, redirectURI string, scope ...string) string {
redURL, err := Tester.CreateOIDCAuthRequest(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
require.NoError(t, err)
return redURL
}
func createAuthRequestImplicit(t testing.TB, clientID, redirectURI string, scope ...string) string {
redURL, err := Tester.CreateOIDCAuthRequestImplicit(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
require.NoError(t, err)
return redURL
}
func assertOIDCTime(t *testing.T, actual oidc.Time, expected time.Time) {
assertOIDCTimeRange(t, actual, expected, expected)
}
func assertOIDCTimeRange(t *testing.T, actual oidc.Time, expectedStart, expectedEnd time.Time) {
assert.WithinRange(t, actual.AsTime(), expectedStart.Add(-1*time.Second), expectedEnd.Add(1*time.Second))
}