mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-03 04:42:15 +00:00
fix: set refresh token in the RetrieveIdentityIntentResponse (#11613)
# Which Problems Are Solved This PR adds support for storing and retrieving refresh tokens from external identity providers during the IDP intent flow. # How the Problems Are Solved * Updated `idp.proto` (v2 and v2beta): added an optional `refresh_token` field to `IDPOAuthAccessInformation`, which, in turn, is used in `RetrieveIdentityProviderIntentResponse` * Added the `IDPRefreshToken` field to the IDP intent `SucceededEvent` struct, and updated the corresponding constructor to set the refresh token * Added the `IDPRefreshToken` field to `IDPIntentWriteModel`, and updated `reduceOAuthSucceededEvent` to populate refresh token from events * Updated `tokensForSucceededIDPIntent` function to extract and encrypt the refresh token from IDP session, if set * Updated `idpOAuthTokensToPb` function to decrypt refresh token before returning to clients * Updated unit and integration tests # Additional Changes Updated the link to the JWT IDP docs linked in the Console # Additional Context - Closes https://github.com/zitadel/zitadel/issues/11047
This commit is contained in:
@@ -170,7 +170,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
accessToken, idToken, err := tokensForSucceededIDPIntent(idpSession, c.idpConfigEncryption)
|
||||
accessToken, refreshToken, idToken, err := tokensForSucceededIDPIntent(idpSession, c.idpConfigEncryption)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -186,6 +186,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
|
||||
idpUser.GetPreferredUsername(),
|
||||
userID,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
idToken,
|
||||
idpSession.ExpiresAt(),
|
||||
)
|
||||
@@ -295,8 +296,9 @@ func (c *Commands) GetIntentWriteModel(ctx context.Context, id, resourceOwner st
|
||||
return writeModel, err
|
||||
}
|
||||
|
||||
// tokensForSucceededIDPIntent extracts the oidc.Tokens if available (and encrypts the access_token) for the succeeded event payload
|
||||
func tokensForSucceededIDPIntent(session idp.Session, encryptionAlg crypto.EncryptionAlgorithm) (*crypto.CryptoValue, string, error) {
|
||||
// tokensForSucceededIDPIntent extracts the oidc.Tokens if available for the succeeded event payload.
|
||||
// It also encrypts the access token and refresh token (if set).
|
||||
func tokensForSucceededIDPIntent(session idp.Session, encryptionAlg crypto.EncryptionAlgorithm) (*crypto.CryptoValue, *crypto.CryptoValue, string, error) {
|
||||
var tokens *oidc.Tokens[*oidc.IDTokenClaims]
|
||||
switch s := session.(type) {
|
||||
case *oauth.Session:
|
||||
@@ -312,11 +314,25 @@ func tokensForSucceededIDPIntent(session idp.Session, encryptionAlg crypto.Encry
|
||||
case *apple.Session:
|
||||
tokens = s.Tokens
|
||||
default:
|
||||
return nil, "", nil
|
||||
return nil, nil, "", nil
|
||||
}
|
||||
if tokens == nil {
|
||||
return nil, nil, "", nil
|
||||
}
|
||||
if tokens.Token == nil || tokens.AccessToken == "" {
|
||||
return nil, tokens.IDToken, nil
|
||||
return nil, nil, tokens.IDToken, nil
|
||||
}
|
||||
accessToken, err := crypto.Encrypt([]byte(tokens.AccessToken), encryptionAlg)
|
||||
return accessToken, tokens.IDToken, err
|
||||
if err != nil {
|
||||
return nil, nil, tokens.IDToken, err
|
||||
}
|
||||
var refreshToken *crypto.CryptoValue
|
||||
if tokens.RefreshToken != "" {
|
||||
refreshToken, err = crypto.Encrypt([]byte(tokens.RefreshToken), encryptionAlg)
|
||||
if err != nil {
|
||||
return nil, nil, tokens.IDToken, err
|
||||
}
|
||||
}
|
||||
|
||||
return accessToken, refreshToken, tokens.IDToken, nil
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ type IDPIntentWriteModel struct {
|
||||
UserID string
|
||||
LoginHint string
|
||||
|
||||
IDPAccessToken *crypto.CryptoValue
|
||||
IDPIDToken string
|
||||
IDPAccessToken *crypto.CryptoValue
|
||||
IDPRefreshToken *crypto.CryptoValue
|
||||
IDPIDToken string
|
||||
|
||||
IDPEntryAttributes map[string][]string
|
||||
|
||||
@@ -131,6 +132,7 @@ func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededE
|
||||
wm.IDPUserID = e.IDPUserID
|
||||
wm.IDPUserName = e.IDPUserName
|
||||
wm.IDPAccessToken = e.IDPAccessToken
|
||||
wm.IDPRefreshToken = e.IDPRefreshToken
|
||||
wm.IDPIDToken = e.IDPIDToken
|
||||
wm.State = domain.IDPIntentStateSucceeded
|
||||
wm.succeededAt = e.CreationDate()
|
||||
|
||||
@@ -1105,6 +1105,12 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("refreshToken"),
|
||||
},
|
||||
"idToken",
|
||||
time.Time{},
|
||||
)
|
||||
@@ -1119,7 +1125,8 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
|
||||
idpSession: &openid.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
AccessToken: "accessToken",
|
||||
RefreshToken: "refreshToken",
|
||||
},
|
||||
IDToken: "idToken",
|
||||
},
|
||||
@@ -1501,9 +1508,10 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
encryptionAlg crypto.EncryptionAlgorithm
|
||||
}
|
||||
type res struct {
|
||||
accessToken *crypto.CryptoValue
|
||||
idToken string
|
||||
err error
|
||||
accessToken *crypto.CryptoValue
|
||||
refreshToken *crypto.CryptoValue
|
||||
idToken string
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1517,9 +1525,10 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
res{
|
||||
accessToken: nil,
|
||||
idToken: "",
|
||||
err: nil,
|
||||
accessToken: nil,
|
||||
refreshToken: nil,
|
||||
idToken: "",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1539,13 +1548,44 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
}(),
|
||||
},
|
||||
res{
|
||||
accessToken: nil,
|
||||
idToken: "",
|
||||
err: zerrors.ThrowInternal(nil, "id", "encryption failed"),
|
||||
accessToken: nil,
|
||||
refreshToken: nil,
|
||||
idToken: "",
|
||||
err: zerrors.ThrowInternal(nil, "id", "encryption failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"oauth tokens",
|
||||
args{
|
||||
&oauth.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
RefreshToken: "refreshToken",
|
||||
},
|
||||
},
|
||||
},
|
||||
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
res{
|
||||
accessToken: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
refreshToken: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("refreshToken"),
|
||||
},
|
||||
idToken: "",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"oauth tokens - no refresh token",
|
||||
args{
|
||||
&oauth.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
@@ -1563,8 +1603,9 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
idToken: "",
|
||||
err: nil,
|
||||
refreshToken: nil,
|
||||
idToken: "",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1573,7 +1614,8 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
&openid.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
AccessToken: "accessToken",
|
||||
RefreshToken: "refreshToken",
|
||||
},
|
||||
IDToken: "idToken",
|
||||
},
|
||||
@@ -1587,6 +1629,12 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
refreshToken: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("refreshToken"),
|
||||
},
|
||||
idToken: "idToken",
|
||||
err: nil,
|
||||
},
|
||||
@@ -1602,9 +1650,10 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
res{
|
||||
accessToken: nil,
|
||||
idToken: "idToken",
|
||||
err: nil,
|
||||
accessToken: nil,
|
||||
refreshToken: nil,
|
||||
idToken: "idToken",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1628,8 +1677,9 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
idToken: "",
|
||||
err: nil,
|
||||
refreshToken: nil,
|
||||
idToken: "",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1639,7 +1689,8 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
OAuthSession: &oauth.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
AccessToken: "accessToken",
|
||||
RefreshToken: "refreshToken",
|
||||
},
|
||||
IDToken: "idToken",
|
||||
},
|
||||
@@ -1654,6 +1705,12 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
refreshToken: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("refreshToken"),
|
||||
},
|
||||
idToken: "idToken",
|
||||
err: nil,
|
||||
},
|
||||
@@ -1661,9 +1718,10 @@ func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotAccessToken, gotIDToken, err := tokensForSucceededIDPIntent(tt.args.session, tt.args.encryptionAlg)
|
||||
gotAccessToken, refreshToken, gotIDToken, err := tokensForSucceededIDPIntent(tt.args.session, tt.args.encryptionAlg)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.accessToken, gotAccessToken)
|
||||
assert.Equal(t, tt.res.refreshToken, refreshToken)
|
||||
assert.Equal(t, tt.res.idToken, gotIDToken)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -742,6 +742,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
"idpUserName",
|
||||
"userID2",
|
||||
nil,
|
||||
nil,
|
||||
"",
|
||||
time.Now().Add(time.Hour),
|
||||
),
|
||||
@@ -823,6 +824,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
"idpUsername",
|
||||
"userID",
|
||||
nil,
|
||||
nil,
|
||||
"",
|
||||
time.Now().Add(time.Hour),
|
||||
),
|
||||
@@ -878,6 +880,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
"idpUsername",
|
||||
"userID",
|
||||
nil,
|
||||
nil,
|
||||
"",
|
||||
time.Now().Add(-time.Hour),
|
||||
),
|
||||
@@ -928,6 +931,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
"idpUsername",
|
||||
"userID",
|
||||
nil,
|
||||
nil,
|
||||
"",
|
||||
time.Now().Add(time.Hour),
|
||||
),
|
||||
@@ -1005,6 +1009,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
"idpUsername",
|
||||
"",
|
||||
nil,
|
||||
nil,
|
||||
"",
|
||||
time.Now().Add(time.Hour),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user