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:
Gayathri Vijayan
2026-02-19 06:06:30 +00:00
committed by GitHub
parent a5a5b65931
commit 6ad1ef859b
13 changed files with 224 additions and 77 deletions
+22 -6
View File
@@ -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
}
+4 -2
View File
@@ -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()
+78 -20
View File
@@ -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)
})
}
+5
View File
@@ -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),
),