Skip to content

Commit

Permalink
adds federated identity support for keycloak_user (#274)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomrutsaert authored Apr 17, 2020
1 parent 0751823 commit 85f78e0
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 1 deletion.
95 changes: 95 additions & 0 deletions example/federated_user_example.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
resource "keycloak_realm" "source_realm" {
realm = "source_realm"
enabled = true
}

resource "keycloak_openid_client" "destination_client" {
realm_id = "${keycloak_realm.source_realm.id}"
name = "destination_client"
client_id = "destination_client"
client_secret = "secret"
description = "a client used by the destination realm"
access_type = "CONFIDENTIAL"
standard_flow_enabled = true
valid_redirect_uris = [
"http://localhost:8080/*",
]
}

//do not get confused this just to have multiple federate idps on the destination realm
resource "keycloak_openid_client" "destination_double_client" {
realm_id = "${keycloak_realm.source_realm.id}"
name = "destination_double_client"
client_id = "destination_double_client"
client_secret = "secret2"
description = "a second client used by the destination realm"
access_type = "CONFIDENTIAL"
standard_flow_enabled = true
valid_redirect_uris = [
"http://localhost:8080/*",
]
}

resource "keycloak_user" "source_user" {
realm_id = "${keycloak_realm.source_realm.id}"
username = "source"
email = "[email protected]"
first_name = "source"
last_name = "source"
initial_password {
value = "source"
temporary = false
}
}

resource "keycloak_realm" "destination_realm" {
realm = "destination_realm"
enabled = true
}

resource keycloak_oidc_identity_provider source_oidc_idp {
realm = "${keycloak_realm.destination_realm.id}"
alias = "source"
authorization_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/auth"
token_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/token"
user_info_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/userinfo"
jwks_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/certs"
validate_signature = true
client_id = "${keycloak_openid_client.destination_client.client_id}"
client_secret = "${keycloak_openid_client.destination_client.client_secret}"
default_scopes = "openid"
}

//do not get confused this second idp towards source_realm, this could a completly different idp
resource keycloak_oidc_identity_provider second_source_oidc_idp {
realm = "${keycloak_realm.destination_realm.id}"
alias = "source2"
authorization_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/auth"
token_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/token"
user_info_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/userinfo"
jwks_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/certs"
validate_signature = true
client_id = "${keycloak_openid_client.destination_double_client.client_id}"
client_secret = "${keycloak_openid_client.destination_double_client.client_secret}"
default_scopes = "openid"
}

resource "keycloak_user" "destination_user" {
realm_id = "${keycloak_realm.destination_realm.id}"
username = "my_destination_username"
email = "[email protected]"
first_name = "Destination_source"
last_name = "Destination_source"
//federated link through source idp
federated_identity {
identity_provider = "${keycloak_oidc_identity_provider.source_oidc_idp.alias}"
user_id = "${keycloak_user.source_user.id}"
user_name = "${keycloak_user.source_user.username}"
}
//federated link through second source idp
federated_identity {
identity_provider = "${keycloak_oidc_identity_provider.second_source_oidc_idp.alias}"
user_id = "${keycloak_user.source_user.id}"
user_name = "${keycloak_user.source_user.username}"
}
}
31 changes: 30 additions & 1 deletion keycloak/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ func (keycloakClient *KeycloakClient) NewUser(user *User) error {

user.Id = getIdFromLocationHeader(location)

for _, federatedIdentity := range user.FederatedIdentities {
_, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/users/%s/federated-identity/%s", user.RealmId, user.Id, federatedIdentity.IdentityProvider), federatedIdentity)
if err != nil {
return err
}
}

return nil
}

Expand Down Expand Up @@ -111,7 +118,29 @@ func (keycloakClient *KeycloakClient) GetUser(realmId, id string) (*User, error)
}

func (keycloakClient *KeycloakClient) UpdateUser(user *User) error {
return keycloakClient.put(fmt.Sprintf("/realms/%s/users/%s", user.RealmId, user.Id), user)
err := keycloakClient.put(fmt.Sprintf("/realms/%s/users/%s", user.RealmId, user.Id), user)
if err != nil {
return err
}

var federatedIdentities []*FederatedIdentity
err = keycloakClient.get(fmt.Sprintf("/realms/%s/users/%s/federated-identity", user.RealmId, user.Id), &federatedIdentities, nil)
if err != nil {
return err
}

for _, federatedIdentity := range federatedIdentities {
keycloakClient.delete(fmt.Sprintf("/realms/%s/users/%s/federated-identity/%s", user.RealmId, user.Id, federatedIdentity.IdentityProvider), nil)
}

for _, federatedIdentity := range user.FederatedIdentities {
_, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/users/%s/federated-identity/%s", user.RealmId, user.Id, federatedIdentity.IdentityProvider), federatedIdentity)
if err != nil {
return err
}
}

return nil
}

func (keycloakClient *KeycloakClient) DeleteUser(realmId, id string) error {
Expand Down
103 changes: 103 additions & 0 deletions provider/resource_keycloak_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,55 @@ func TestAccKeycloakUser_validateLowercaseUsernames(t *testing.T) {
})
}

func TestAccKeycloakUser_federatedLink(t *testing.T) {
sourceUserName := "terraform-source-user-" + acctest.RandString(10)
sourceUserName2 := "terraform-source-user2-" + acctest.RandString(10)
destinationRealmName := "terraform-dest-" + acctest.RandString(10)

resourceName := "keycloak_user.destination_user"

resource.Test(t, resource.TestCase{
Providers: testAccProviders,
PreCheck: func() { testAccPreCheck(t) },
CheckDestroy: testAccCheckKeycloakUserDestroy(),
Steps: []resource.TestStep{
{
Config: testKeycloakUser_FederationLink(sourceUserName, destinationRealmName),
Check: testAccCheckKeycloakUserHasFederationLinkWithSourceUserName(resourceName, sourceUserName),
},
{
Config: testKeycloakUser_FederationLink(sourceUserName2, destinationRealmName),
Check: testAccCheckKeycloakUserHasFederationLinkWithSourceUserName(resourceName, sourceUserName2),
},
},
})
}

func testAccCheckKeycloakUserHasFederationLinkWithSourceUserName(resourceName, sourceUserName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
fetchedUser, err := getUserFromState(s, resourceName)
if err != nil {
return err
}

var found bool = false
for _, federatedIdentity := range fetchedUser.FederatedIdentities {
if federatedIdentity.UserName == sourceUserName {
found = true
}
if !found {
return fmt.Errorf("user had unexpected federatedLink %s or unexpected username %s", federatedIdentity.IdentityProvider, federatedIdentity.UserName)
}
}

if !found {
return fmt.Errorf("user had no federatedLink, but one was expected")
}

return nil
}
}

func testAccCheckKeycloakUserExists(resourceName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
_, err := getUserFromState(s, resourceName)
Expand Down Expand Up @@ -488,3 +537,57 @@ resource "keycloak_user" "user" {
}
`, user.RealmId, user.Username, user.Email, user.FirstName, user.LastName, user.Enabled, user.EmailVerified)
}

func testKeycloakUser_FederationLink(sourceRealmUserName, destinationRealmId string) string {
return fmt.Sprintf(`
resource "keycloak_realm" "source_realm" {
realm = "source_test_realm"
enabled = true
}
resource "keycloak_openid_client" "destination_client" {
realm_id = "${keycloak_realm.source_realm.id}"
client_id = "destination_client"
client_secret = "secret"
access_type = "CONFIDENTIAL"
standard_flow_enabled = true
valid_redirect_uris = [
"http://localhost:8080/*",
]
}
resource "keycloak_user" "source_user" {
realm_id = "${keycloak_realm.source_realm.id}"
username = "%s"
initial_password {
value = "source"
temporary = false
}
}
resource "keycloak_realm" "destination_realm" {
realm = "%s"
enabled = true
}
resource keycloak_oidc_identity_provider source_oidc_idp {
realm = "${keycloak_realm.destination_realm.id}"
alias = "source"
authorization_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/auth"
token_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/token"
client_id = "${keycloak_openid_client.destination_client.client_id}"
client_secret = "${keycloak_openid_client.destination_client.client_secret}"
default_scopes = "openid"
}
resource "keycloak_user" "destination_user" {
realm_id = "${keycloak_realm.destination_realm.id}"
username = "my_destination_username"
federated_identity {
identity_provider = "${keycloak_oidc_identity_provider.source_oidc_idp.alias}"
user_id = "${keycloak_user.source_user.id}"
user_name = "${keycloak_user.source_user.username}"
}
}
`, sourceRealmUserName, destinationRealmId)
}

0 comments on commit 85f78e0

Please sign in to comment.