Skip to content

Commit 186f7cf

Browse files
KyriosGN0sschu
andauthored
expose EnableStandardTokenExchange and allow refresh token in standard token exchange in keycloak_openid_client (#1229)
* support settings allow_refresh_token_in_standartd_token_exhange and expose standard_token_exchange_enabled in openid_client Signed-off-by: AvivGuiser <[email protected]> * update tests Signed-off-by: AvivGuiser <[email protected]> * fix tests Signed-off-by: AvivGuiser <[email protected]> * skip test unless keycloak is version 26.2 Signed-off-by: AvivGuiser <[email protected]> * fix datasource Signed-off-by: AvivGuiser <[email protected]> * fix tests Signed-off-by: AvivGuiser <[email protected]> --------- Signed-off-by: AvivGuiser <[email protected]> Co-authored-by: Sebastian Schuster <[email protected]>
1 parent eac736b commit 186f7cf

File tree

4 files changed

+182
-47
lines changed

4 files changed

+182
-47
lines changed

keycloak/openid_client.go

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,29 +61,30 @@ type OpenidClient struct {
6161
}
6262

6363
type OpenidClientAttributes struct {
64-
PkceCodeChallengeMethod string `json:"pkce.code.challenge.method"`
65-
ExcludeSessionStateFromAuthResponse types.KeycloakBoolQuoted `json:"exclude.session.state.from.auth.response"`
66-
ExcludeIssuerFromAuthResponse types.KeycloakBoolQuoted `json:"exclude.issuer.from.auth.response"`
67-
AccessTokenLifespan string `json:"access.token.lifespan"`
68-
LoginTheme string `json:"login_theme"`
69-
ClientOfflineSessionIdleTimeout string `json:"client.offline.session.idle.timeout,omitempty"`
70-
DisplayOnConsentScreen types.KeycloakBoolQuoted `json:"display.on.consent.screen"`
71-
ConsentScreenText string `json:"consent.screen.text"`
72-
ClientOfflineSessionMaxLifespan string `json:"client.offline.session.max.lifespan,omitempty"`
73-
ClientSessionIdleTimeout string `json:"client.session.idle.timeout,omitempty"`
74-
ClientSessionMaxLifespan string `json:"client.session.max.lifespan,omitempty"`
75-
UseRefreshTokens types.KeycloakBoolQuoted `json:"use.refresh.tokens"`
76-
UseRefreshTokensClientCredentials types.KeycloakBoolQuoted `json:"client_credentials.use_refresh_token"`
77-
BackchannelLogoutUrl string `json:"backchannel.logout.url"`
78-
FrontchannelLogoutUrl string `json:"frontchannel.logout.url"`
79-
BackchannelLogoutRevokeOfflineTokens types.KeycloakBoolQuoted `json:"backchannel.logout.revoke.offline.tokens"`
80-
BackchannelLogoutSessionRequired types.KeycloakBoolQuoted `json:"backchannel.logout.session.required"`
81-
ExtraConfig map[string]interface{} `json:"-"`
82-
Oauth2DeviceAuthorizationGrantEnabled types.KeycloakBoolQuoted `json:"oauth2.device.authorization.grant.enabled"`
83-
Oauth2DeviceCodeLifespan string `json:"oauth2.device.code.lifespan,omitempty"`
84-
Oauth2DevicePollingInterval string `json:"oauth2.device.polling.interval,omitempty"`
85-
PostLogoutRedirectUris types.KeycloakSliceHashDelimited `json:"post.logout.redirect.uris,omitempty"`
86-
StandardTokenExchangeEnabled types.KeycloakBoolQuoted `json:"standard.token.exchange.enabled,omitempty"`
64+
PkceCodeChallengeMethod string `json:"pkce.code.challenge.method"`
65+
ExcludeSessionStateFromAuthResponse types.KeycloakBoolQuoted `json:"exclude.session.state.from.auth.response"`
66+
ExcludeIssuerFromAuthResponse types.KeycloakBoolQuoted `json:"exclude.issuer.from.auth.response"`
67+
AccessTokenLifespan string `json:"access.token.lifespan"`
68+
LoginTheme string `json:"login_theme"`
69+
ClientOfflineSessionIdleTimeout string `json:"client.offline.session.idle.timeout,omitempty"`
70+
DisplayOnConsentScreen types.KeycloakBoolQuoted `json:"display.on.consent.screen"`
71+
ConsentScreenText string `json:"consent.screen.text"`
72+
ClientOfflineSessionMaxLifespan string `json:"client.offline.session.max.lifespan,omitempty"`
73+
ClientSessionIdleTimeout string `json:"client.session.idle.timeout,omitempty"`
74+
ClientSessionMaxLifespan string `json:"client.session.max.lifespan,omitempty"`
75+
UseRefreshTokens types.KeycloakBoolQuoted `json:"use.refresh.tokens"`
76+
UseRefreshTokensClientCredentials types.KeycloakBoolQuoted `json:"client_credentials.use_refresh_token"`
77+
BackchannelLogoutUrl string `json:"backchannel.logout.url"`
78+
FrontchannelLogoutUrl string `json:"frontchannel.logout.url"`
79+
BackchannelLogoutRevokeOfflineTokens types.KeycloakBoolQuoted `json:"backchannel.logout.revoke.offline.tokens"`
80+
BackchannelLogoutSessionRequired types.KeycloakBoolQuoted `json:"backchannel.logout.session.required"`
81+
ExtraConfig map[string]interface{} `json:"-"`
82+
Oauth2DeviceAuthorizationGrantEnabled types.KeycloakBoolQuoted `json:"oauth2.device.authorization.grant.enabled"`
83+
Oauth2DeviceCodeLifespan string `json:"oauth2.device.code.lifespan,omitempty"`
84+
Oauth2DevicePollingInterval string `json:"oauth2.device.polling.interval,omitempty"`
85+
PostLogoutRedirectUris types.KeycloakSliceHashDelimited `json:"post.logout.redirect.uris,omitempty"`
86+
StandardTokenExchangeEnabled types.KeycloakBoolQuoted `json:"standard.token.exchange.enabled,omitempty"`
87+
AllowRefreshTokenInStandardTokenExchange string `json:"standard.token.exchange.enableRefreshRequestedTokenType,omitempty"`
8788
}
8889

8990
type OpenidAuthenticationFlowBindingOverrides struct {

provider/data_source_keycloak_openid_client.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,14 @@ func dataSourceKeycloakOpenidClient() *schema.Resource {
204204
Type: schema.TypeBool,
205205
Computed: true,
206206
},
207+
"standard_token_exchange_enabled": {
208+
Type: schema.TypeBool,
209+
Computed: true,
210+
},
211+
"allow_refresh_token_in_standard_token_exchange": {
212+
Type: schema.TypeString,
213+
Computed: true,
214+
},
207215
"backchannel_logout_url": {
208216
Type: schema.TypeString,
209217
Computed: true,

provider/resource_keycloak_openid_client.go

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"github.com/hashicorp/go-cty/cty"
87
"reflect"
98
"strings"
109

10+
"github.com/hashicorp/go-cty/cty"
11+
1112
"dario.cat/mergo"
1213
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1314
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
@@ -283,6 +284,15 @@ func resourceKeycloakOpenidClient() *schema.Resource {
283284
Optional: true,
284285
Default: false,
285286
},
287+
"standard_token_exchange_enabled": {
288+
Type: schema.TypeBool,
289+
Optional: true,
290+
Default: false,
291+
},
292+
"allow_refresh_token_in_standard_token_exchange": {
293+
Type: schema.TypeString,
294+
Optional: true,
295+
},
286296
"frontchannel_logout_url": {
287297
Type: schema.TypeString,
288298
Optional: true,
@@ -382,28 +392,30 @@ func getOpenidClientFromData(data *schema.ResourceData) (*keycloak.OpenidClient,
382392
FrontChannelLogoutEnabled: data.Get("frontchannel_logout_enabled").(bool),
383393
FullScopeAllowed: data.Get("full_scope_allowed").(bool),
384394
Attributes: keycloak.OpenidClientAttributes{
385-
PkceCodeChallengeMethod: data.Get("pkce_code_challenge_method").(string),
386-
ExcludeSessionStateFromAuthResponse: types.KeycloakBoolQuoted(data.Get("exclude_session_state_from_auth_response").(bool)),
387-
ExcludeIssuerFromAuthResponse: types.KeycloakBoolQuoted(data.Get("exclude_issuer_from_auth_response").(bool)),
388-
AccessTokenLifespan: data.Get("access_token_lifespan").(string),
389-
LoginTheme: data.Get("login_theme").(string),
390-
ClientOfflineSessionIdleTimeout: data.Get("client_offline_session_idle_timeout").(string),
391-
ClientOfflineSessionMaxLifespan: data.Get("client_offline_session_max_lifespan").(string),
392-
ClientSessionIdleTimeout: data.Get("client_session_idle_timeout").(string),
393-
ClientSessionMaxLifespan: data.Get("client_session_max_lifespan").(string),
394-
UseRefreshTokens: types.KeycloakBoolQuoted(data.Get("use_refresh_tokens").(bool)),
395-
UseRefreshTokensClientCredentials: types.KeycloakBoolQuoted(data.Get("use_refresh_tokens_client_credentials").(bool)),
396-
FrontchannelLogoutUrl: data.Get("frontchannel_logout_url").(string),
397-
BackchannelLogoutUrl: data.Get("backchannel_logout_url").(string),
398-
BackchannelLogoutRevokeOfflineTokens: types.KeycloakBoolQuoted(data.Get("backchannel_logout_revoke_offline_sessions").(bool)),
399-
BackchannelLogoutSessionRequired: types.KeycloakBoolQuoted(data.Get("backchannel_logout_session_required").(bool)),
400-
ExtraConfig: getExtraConfigFromData(data),
401-
Oauth2DeviceAuthorizationGrantEnabled: types.KeycloakBoolQuoted(data.Get("oauth2_device_authorization_grant_enabled").(bool)),
402-
Oauth2DeviceCodeLifespan: data.Get("oauth2_device_code_lifespan").(string),
403-
Oauth2DevicePollingInterval: data.Get("oauth2_device_polling_interval").(string),
404-
ConsentScreenText: data.Get("consent_screen_text").(string),
405-
DisplayOnConsentScreen: types.KeycloakBoolQuoted(data.Get("display_on_consent_screen").(bool)),
406-
PostLogoutRedirectUris: types.KeycloakSliceHashDelimited(validPostLogoutRedirectUris),
395+
PkceCodeChallengeMethod: data.Get("pkce_code_challenge_method").(string),
396+
ExcludeSessionStateFromAuthResponse: types.KeycloakBoolQuoted(data.Get("exclude_session_state_from_auth_response").(bool)),
397+
ExcludeIssuerFromAuthResponse: types.KeycloakBoolQuoted(data.Get("exclude_issuer_from_auth_response").(bool)),
398+
AccessTokenLifespan: data.Get("access_token_lifespan").(string),
399+
LoginTheme: data.Get("login_theme").(string),
400+
ClientOfflineSessionIdleTimeout: data.Get("client_offline_session_idle_timeout").(string),
401+
ClientOfflineSessionMaxLifespan: data.Get("client_offline_session_max_lifespan").(string),
402+
ClientSessionIdleTimeout: data.Get("client_session_idle_timeout").(string),
403+
ClientSessionMaxLifespan: data.Get("client_session_max_lifespan").(string),
404+
UseRefreshTokens: types.KeycloakBoolQuoted(data.Get("use_refresh_tokens").(bool)),
405+
UseRefreshTokensClientCredentials: types.KeycloakBoolQuoted(data.Get("use_refresh_tokens_client_credentials").(bool)),
406+
StandardTokenExchangeEnabled: types.KeycloakBoolQuoted(data.Get("standard_token_exchange_enabled").(bool)),
407+
AllowRefreshTokenInStandardTokenExchange: data.Get("allow_refresh_token_in_standard_token_exchange").(string),
408+
FrontchannelLogoutUrl: data.Get("frontchannel_logout_url").(string),
409+
BackchannelLogoutUrl: data.Get("backchannel_logout_url").(string),
410+
BackchannelLogoutRevokeOfflineTokens: types.KeycloakBoolQuoted(data.Get("backchannel_logout_revoke_offline_sessions").(bool)),
411+
BackchannelLogoutSessionRequired: types.KeycloakBoolQuoted(data.Get("backchannel_logout_session_required").(bool)),
412+
ExtraConfig: getExtraConfigFromData(data),
413+
Oauth2DeviceAuthorizationGrantEnabled: types.KeycloakBoolQuoted(data.Get("oauth2_device_authorization_grant_enabled").(bool)),
414+
Oauth2DeviceCodeLifespan: data.Get("oauth2_device_code_lifespan").(string),
415+
Oauth2DevicePollingInterval: data.Get("oauth2_device_polling_interval").(string),
416+
ConsentScreenText: data.Get("consent_screen_text").(string),
417+
DisplayOnConsentScreen: types.KeycloakBoolQuoted(data.Get("display_on_consent_screen").(bool)),
418+
PostLogoutRedirectUris: types.KeycloakSliceHashDelimited(validPostLogoutRedirectUris),
407419
},
408420
ValidRedirectUris: validRedirectUris,
409421
WebOrigins: webOrigins,
@@ -506,6 +518,8 @@ func setOpenidClientData(ctx context.Context, keycloakClient *keycloak.KeycloakC
506518
data.Set("login_theme", client.Attributes.LoginTheme)
507519
data.Set("use_refresh_tokens", client.Attributes.UseRefreshTokens)
508520
data.Set("use_refresh_tokens_client_credentials", client.Attributes.UseRefreshTokensClientCredentials)
521+
data.Set("standard_token_exchange_enabled", client.Attributes.StandardTokenExchangeEnabled)
522+
data.Set("allow_refresh_token_in_standard_token_exchange", client.Attributes.AllowRefreshTokenInStandardTokenExchange)
509523
data.Set("oauth2_device_authorization_grant_enabled", client.Attributes.Oauth2DeviceAuthorizationGrantEnabled)
510524
data.Set("oauth2_device_code_lifespan", client.Attributes.Oauth2DeviceCodeLifespan)
511525
data.Set("oauth2_device_polling_interval", client.Attributes.Oauth2DevicePollingInterval)

provider/resource_keycloak_openid_client_test.go

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ package provider
22

33
import (
44
"fmt"
5-
"github.com/keycloak/terraform-provider-keycloak/keycloak/types"
65
"regexp"
76
"strconv"
87
"strings"
98
"testing"
109

10+
"github.com/keycloak/terraform-provider-keycloak/keycloak/types"
11+
1112
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
1213
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1314
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
@@ -792,6 +793,54 @@ func TestAccKeycloakOpenidClient_useRefreshTokens(t *testing.T) {
792793
})
793794
}
794795

796+
func TestAccKeycloakOpenidClient_enableStandardTokenExchange(t *testing.T) {
797+
if ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(testCtx, keycloak.Version_26_2); !ok {
798+
t.Skip()
799+
}
800+
t.Parallel()
801+
clientId := acctest.RandomWithPrefix("tf-acc")
802+
803+
resource.Test(t, resource.TestCase{
804+
ProviderFactories: testAccProviderFactories,
805+
PreCheck: func() { testAccPreCheck(t) },
806+
CheckDestroy: testAccCheckKeycloakOpenidClientDestroy(),
807+
Steps: []resource.TestStep{
808+
{
809+
Config: testKeycloakOpenidClient_enableStandardTokenExchange(clientId, true),
810+
Check: testAccCheckKeycloakOpenidClientEnableStandardTokenExchange("keycloak_openid_client.client", true),
811+
},
812+
{
813+
Config: testKeycloakOpenidClient_enableStandardTokenExchange(clientId, false),
814+
Check: testAccCheckKeycloakOpenidClientEnableStandardTokenExchange("keycloak_openid_client.client", false),
815+
},
816+
},
817+
})
818+
}
819+
820+
func TestAccKeycloakOpenidClient_allowRefreshTokenInStandardExchange(t *testing.T) {
821+
if ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(testCtx, keycloak.Version_26_2); !ok {
822+
t.Skip()
823+
}
824+
t.Parallel()
825+
clientId := acctest.RandomWithPrefix("tf-acc")
826+
827+
resource.Test(t, resource.TestCase{
828+
ProviderFactories: testAccProviderFactories,
829+
PreCheck: func() { testAccPreCheck(t) },
830+
CheckDestroy: testAccCheckKeycloakOpenidClientDestroy(),
831+
Steps: []resource.TestStep{
832+
{
833+
Config: testKeycloakOpenidClient_allowRefreshTokenInStandardExchange(clientId, "SAME_SESSION"),
834+
Check: testAccCheckKeycloakOpenidClientAllowRefreshTokenInStandardExchange("keycloak_openid_client.client", "SAME_SESSION"),
835+
},
836+
{
837+
Config: testKeycloakOpenidClient_allowRefreshTokenInStandardExchange(clientId, ""),
838+
Check: testAccCheckKeycloakOpenidClientAllowRefreshTokenInStandardExchange("keycloak_openid_client.client", ""),
839+
},
840+
},
841+
})
842+
}
843+
795844
func TestAccKeycloakOpenidClient_useRefreshTokensClientCredentials(t *testing.T) {
796845
t.Parallel()
797846
clientId := acctest.RandomWithPrefix("tf-acc")
@@ -1277,6 +1326,36 @@ func testAccCheckKeycloakOpenidClientUseRefreshTokens(resourceName string, useRe
12771326
}
12781327
}
12791328

1329+
func testAccCheckKeycloakOpenidClientEnableStandardTokenExchange(resourceName string, enableStandardTokenExchange bool) resource.TestCheckFunc {
1330+
return func(s *terraform.State) error {
1331+
client, err := getOpenidClientFromState(s, resourceName)
1332+
if err != nil {
1333+
return err
1334+
}
1335+
1336+
if client.Attributes.StandardTokenExchangeEnabled != types.KeycloakBoolQuoted(enableStandardTokenExchange) {
1337+
return fmt.Errorf("expected openid client to have enable standard token exchange set to %t, but got %v", enableStandardTokenExchange, client.Attributes.StandardTokenExchangeEnabled)
1338+
}
1339+
1340+
return nil
1341+
}
1342+
}
1343+
1344+
func testAccCheckKeycloakOpenidClientAllowRefreshTokenInStandardExchange(resourceName string, AllowRefreshTokenInStandardTokenExchange string) resource.TestCheckFunc {
1345+
return func(s *terraform.State) error {
1346+
client, err := getOpenidClientFromState(s, resourceName)
1347+
if err != nil {
1348+
return err
1349+
}
1350+
1351+
if client.Attributes.AllowRefreshTokenInStandardTokenExchange != AllowRefreshTokenInStandardTokenExchange {
1352+
return fmt.Errorf("expected openid client to have enable standard token exchange set to %s, but got %v", AllowRefreshTokenInStandardTokenExchange, client.Attributes.AllowRefreshTokenInStandardTokenExchange)
1353+
}
1354+
1355+
return nil
1356+
}
1357+
}
1358+
12801359
func testAccCheckKeycloakOpenidClientUseRefreshTokensClientCredentials(resourceName string, useRefreshTokensClientCredentials bool) resource.TestCheckFunc {
12811360
return func(s *terraform.State) error {
12821361
client, err := getOpenidClientFromState(s, resourceName)
@@ -1834,6 +1913,39 @@ resource "keycloak_openid_client" "client" {
18341913
`, testAccRealm.Realm, clientId, useRefreshTokens)
18351914
}
18361915

1916+
func testKeycloakOpenidClient_enableStandardTokenExchange(clientId string, enableStandardTokenExchange bool) string {
1917+
1918+
return fmt.Sprintf(`
1919+
data "keycloak_realm" "realm" {
1920+
realm = "%s"
1921+
}
1922+
1923+
resource "keycloak_openid_client" "client" {
1924+
client_id = "%s"
1925+
realm_id = data.keycloak_realm.realm.id
1926+
access_type = "CONFIDENTIAL"
1927+
standard_token_exchange_enabled = %t
1928+
}
1929+
`, testAccRealm.Realm, clientId, enableStandardTokenExchange)
1930+
}
1931+
1932+
func testKeycloakOpenidClient_allowRefreshTokenInStandardExchange(clientId string, allowRefreshTokenInStandardTokenExchange string) string {
1933+
1934+
return fmt.Sprintf(`
1935+
data "keycloak_realm" "realm" {
1936+
realm = "%s"
1937+
}
1938+
1939+
resource "keycloak_openid_client" "client" {
1940+
client_id = "%s"
1941+
realm_id = data.keycloak_realm.realm.id
1942+
access_type = "CONFIDENTIAL"
1943+
standard_token_exchange_enabled = true
1944+
allow_refresh_token_in_standard_token_exchange = "%s"
1945+
}
1946+
`, testAccRealm.Realm, clientId, allowRefreshTokenInStandardTokenExchange)
1947+
}
1948+
18371949
func testKeycloakOpenidClient_useRefreshTokensClientCredentials(clientId string, useRefreshTokensClientCredentials bool) string {
18381950

18391951
return fmt.Sprintf(`

0 commit comments

Comments
 (0)