Repository: Nerzal/gocloak
Pull request #120 (2019-12-16 22:31:20)

Adding GetRequestingPartyToken method
I need to be able to get a RPT. This could potentially be rolled into `GetToken` and `TokenOptions` but seemed a bit messy so I made it a different method.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 720a1fd..9568ecd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,7 +26,7 @@ jobs:
     - stage: test
       before_install:
         - docker pull quay.io/keycloak/keycloak
-        - docker run -d -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=secret -e KEYCLOAK_IMPORT=/tmp/gocloak-realm.json -v "`pwd`/testdata/gocloak-realm.json:/tmp/gocloak-realm.json" -p 8080:8080 --name keycloak quay.io/keycloak/keycloak
+        - docker run -d -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=secret -e KEYCLOAK_IMPORT=/tmp/gocloak-realm.json -v "`pwd`/testdata/gocloak-realm.json:/tmp/gocloak-realm.json" -p 8080:8080 --name keycloak quay.io/keycloak/keycloak -Dkeycloak.profile.feature.upload_scripts=enabled
       script:
         - go test -cover -race -coverprofile=coverage.txt -covermode=atomic
       after_success:
diff --git a/README.md b/README.md
index 9a6f9c5..dcaaad8 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,8 @@ or v3 (latest release is v3.10.0):
 ```go
 // GoCloak holds all methods a client should fullfill
 type GoCloak interface {
+	GetRequestingPartyToken(token, realm string, options RequestingPartyTokenOptions) (*JWT, error)
+
 	Login(clientID string, clientSecret string, realm string, username string, password string) (*JWT, error)
 	Logout(clientID, clientSecret, realm, refreshToken string) error
 	LogoutPublicClient(clientID, realm, accessToken, refreshToken string) error
diff --git a/client.go b/client.go
index 82296f1..3168fa3 100644
--- a/client.go
+++ b/client.go
@@ -4,6 +4,7 @@ import (
 	"encoding/base64"
 	"errors"
 	"fmt"
+	"net/url"
 	"strings"
 	"sync"
 	"time"
@@ -289,6 +290,23 @@ func (client *gocloak) GetToken(realm string, options TokenOptions) (*JWT, error
 	return &token, nil
 }
 
+// GetRequestingPartyToken returns a requesting party token with permissions granted by the server
+func (client *gocloak) GetRequestingPartyToken(token, realm string, options RequestingPartyTokenOptions) (*JWT, error) {
+	var res JWT
+
+	resp, err := client.getRequestWithBearerAuth(token).
+		SetFormData(options.FormData()).
+		SetFormDataFromValues(url.Values{"permission": options.Permissions}).
+		SetResult(&res).
+		Post(client.getRealmURL(realm, tokenEndpoint))
+
+	if err := checkForError(resp, err); err != nil {
+		return nil, err
+	}
+
+	return &res, nil
+}
+
 // RefreshToken refreshes the given token
 func (client *gocloak) RefreshToken(refreshToken, clientID, clientSecret, realm string) (*JWT, error) {
 	return client.GetToken(realm, TokenOptions{
diff --git a/gocloak.go b/gocloak.go
index 5e08431..5ebc3e1 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -14,6 +14,8 @@ type GoCloak interface {
 
 	// GetToken returns a token
 	GetToken(realm string, options TokenOptions) (*JWT, error)
+	// GetRequestingPartyToken returns a requesting party token with permissions granted by the server
+	GetRequestingPartyToken(token, realm string, options RequestingPartyTokenOptions) (*JWT, error)
 	// Login sends a request to the token endpoint using user and client credentials
 	Login(clientID, clientSecret, realm, username, password string) (*JWT, error)
 	// Logout sends a request to the logout endpoint using refresh token
diff --git a/models.go b/models.go
index 265a243..a73df62 100644
--- a/models.go
+++ b/models.go
@@ -599,6 +599,36 @@ func (t *TokenOptions) FormData() map[string]string {
 	return res
 }
 
+// RequestingPartyTokenOptions represents the options to obtain a requesting party token
+type RequestingPartyTokenOptions struct {
+	GrantType                   *string  `json:"grant_type"`
+	Ticket                      *string  `json:"ticket,omitempty"`
+	ClaimToken                  *string  `json:"claim_token,omitempty"`
+	ClaimTokenFormat            *string  `json:"claim_token_format,omitempty"`
+	RPT                         *string  `json:"rpt,omitempty"`
+	Permissions                 []string `json:"-"`
+	Audience                    *string  `json:"audience,omitempty"`
+	ResponseIncludeResourceName *bool    `json:"response_include_resource_name,string"`
+	ResponsePermissionsLimit    *uint32  `json:"response_permissions_limit,omitempty"`
+	SubmitRequest               *bool    `json:"submit_request,string,omitempty"`
+	ResponseMode                *string  `json:"response_mode,omitempty"`
+}
+
+// FormData returns a map of options to be used in SetFormData function
+func (t *RequestingPartyTokenOptions) FormData() map[string]string {
+	if NilOrEmpty(t.GrantType) { // required grant type for RPT
+		t.GrantType = StringP("urn:ietf:params:oauth:grant-type:uma-ticket")
+	}
+	if t.ResponseIncludeResourceName == nil { // defaults to true if no value set
+		t.ResponseIncludeResourceName = BoolP(true)
+	}
+
+	m, _ := json.Marshal(t)
+	var res map[string]string
+	_ = json.Unmarshal(m, &res)
+	return res
+}
+
 // UserSessionRepresentation represents a list of user's sessions
 type UserSessionRepresentation struct {
 	Clients    map[string]string `json:"clients,omitempty"`


Test patch:
diff --git a/client_test.go b/client_test.go
index 609a0f3..afe5b77 100644
--- a/client_test.go
+++ b/client_test.go
@@ -604,6 +604,38 @@ func TestGocloak_GetToken(t *testing.T) {
 	assert.NotEmpty(t, newToken.IDToken, "Got an empty if token")
 }
 
+func TestGocloak_GetRequestingPartyToken(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	SetUpTestUser(t, client)
+	newToken, err := client.GetToken(
+		cfg.GoCloak.Realm,
+		TokenOptions{
+			ClientID:      &cfg.GoCloak.ClientID,
+			ClientSecret:  &cfg.GoCloak.ClientSecret,
+			Username:      &cfg.GoCloak.UserName,
+			Password:      &cfg.GoCloak.Password,
+			GrantType:     StringP("password"),
+			ResponseTypes: []string{"token", "id_token"},
+			Scopes:        []string{"openid"},
+		},
+	)
+	assert.NoError(t, err, "Login failed")
+	t.Logf("New token: %+v", *newToken)
+	assert.NotEmpty(t, newToken.IDToken, "Got an empty id token")
+
+	rpt, err := client.GetRequestingPartyToken(
+		newToken.AccessToken,
+		cfg.GoCloak.Realm,
+		RequestingPartyTokenOptions{
+			Audience: &cfg.GoCloak.ClientID,
+		},
+	)
+	assert.NoError(t, err, "Get requesting party token failed")
+	t.Logf("New RPT: %+v", *rpt)
+}
+
 func TestGocloak_LoginClient(t *testing.T) {
 	t.Parallel()
 	cfg := GetConfig(t)
diff --git a/testdata/gocloak-realm.json b/testdata/gocloak-realm.json
index 5bb67e8..606f4a1 100644
--- a/testdata/gocloak-realm.json
+++ b/testdata/gocloak-realm.json
@@ -37,8 +37,8 @@
   "maxDeltaTimeSeconds": 43200,
   "failureFactor": 30,
   "defaultRoles": [
-    "offline_access",
-    "uma_authorization"
+    "uma_authorization",
+    "offline_access"
   ],
   "requiredCredentials": [
     "password"
@@ -53,6 +53,18 @@
     "FreeOTP",
     "Google Authenticator"
   ],
+  "webAuthnPolicyRpEntityName": "keycloak",
+  "webAuthnPolicySignatureAlgorithms": [
+    "ES256"
+  ],
+  "webAuthnPolicyRpId": "",
+  "webAuthnPolicyAttestationConveyancePreference": "not specified",
+  "webAuthnPolicyAuthenticatorAttachment": "not specified",
+  "webAuthnPolicyRequireResidentKey": "not specified",
+  "webAuthnPolicyUserVerificationRequirement": "not specified",
+  "webAuthnPolicyCreateTimeout": 0,
+  "webAuthnPolicyAvoidSameAuthenticatorRegister": false,
+  "webAuthnPolicyAcceptableAaguids": [],
   "scopeMappings": [
     {
       "clientScope": "offline_access",
@@ -89,14 +101,15 @@
       "defaultClientScopes": [
         "web-origins",
         "role_list",
-        "profile",
         "roles",
+        "profile",
         "email"
       ],
       "optionalClientScopes": [
         "address",
         "phone",
-        "offline_access"
+        "offline_access",
+        "microprofile-jwt"
       ]
     },
     {
@@ -126,29 +139,33 @@
       "defaultClientScopes": [
         "web-origins",
         "role_list",
-        "profile",
         "roles",
+        "profile",
         "email"
       ],
       "optionalClientScopes": [
         "address",
         "phone",
-        "offline_access"
+        "offline_access",
+        "microprofile-jwt"
       ]
     },
     {
       "id": "c8482741-e251-4366-9823-48f083f8970a",
       "clientId": "security-admin-console",
       "name": "${client_security-admin-console}",
-      "baseUrl": "/auth/admin/gocloak/console/index.html",
+      "rootUrl": "${authAdminUrl}",
+      "baseUrl": "/admin/gocloak/console/",
       "surrogateAuthRequired": false,
       "enabled": true,
       "clientAuthenticatorType": "client-secret",
       "secret": "**********",
       "redirectUris": [
-        "/auth/admin/gocloak/console/*"
+        "/admin/gocloak/console/*"
+      ],
+      "webOrigins": [
+        "+"
       ],
-      "webOrigins": [],
       "notBefore": 0,
       "bearerOnly": false,
       "consentRequired": false,
@@ -183,14 +200,15 @@
       "defaultClientScopes": [
         "web-origins",
         "role_list",
-        "profile",
         "roles",
+        "profile",
         "email"
       ],
       "optionalClientScopes": [
         "address",
         "phone",
-        "offline_access"
+        "offline_access",
+        "microprofile-jwt"
       ]
     },
     {
@@ -211,6 +229,7 @@
       "implicitFlowEnabled": false,
       "directAccessGrantsEnabled": true,
       "serviceAccountsEnabled": true,
+      "authorizationServicesEnabled": true,
       "publicClient": false,
       "frontchannelLogout": false,
       "protocol": "openid-connect",
@@ -241,6 +260,7 @@
           "consentRequired": false,
           "config": {
             "user.session.note": "clientHost",
+            "userinfo.token.claim": "true",
             "id.token.claim": "true",
             "access.token.claim": "true",
             "claim.name": "clientHost",
@@ -255,6 +275,7 @@
           "consentRequired": false,
           "config": {
             "user.session.note": "clientId",
+            "userinfo.token.claim": "true",
             "id.token.claim": "true",
             "access.token.claim": "true",
             "claim.name": "clientId",
@@ -269,6 +290,7 @@
           "consentRequired": false,
           "config": {
             "user.session.note": "clientAddress",
+            "userinfo.token.claim": "true",
             "id.token.claim": "true",
             "access.token.claim": "true",
             "claim.name": "clientAddress",
@@ -279,15 +301,59 @@
       "defaultClientScopes": [
         "web-origins",
         "role_list",
-        "profile",
         "roles",
+        "profile",
         "email"
       ],
       "optionalClientScopes": [
         "address",
         "phone",
-        "offline_access"
-      ]
+        "offline_access",
+        "microprofile-jwt"
+      ],
+      "authorizationSettings": {
+        "allowRemoteResourceManagement": true,
+        "policyEnforcementMode": "ENFORCING",
+        "resources": [
+          {
+            "name": "Default Resource",
+            "type": "urn:gocloak:resources:default",
+            "ownerManagedAccess": false,
+            "attributes": {},
+            "_id": "067438bb-1d4a-4c4d-b84f-a57344ef2b2b",
+            "uris": [
+              "/*"
+            ]
+          }
+        ],
+        "policies": [
+          {
+            "id": "16fdc33a-7384-4735-8b7e-5d15b8e8bdaf",
+            "name": "Default Policy",
+            "description": "A policy that grants access only for users within this realm",
+            "type": "js",
+            "logic": "POSITIVE",
+            "decisionStrategy": "AFFIRMATIVE",
+            "config": {
+              "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n"
+            }
+          },
+          {
+            "id": "30cfeba8-79c4-493b-97c7-9e8c6e7c62c9",
+            "name": "Default Permission",
+            "description": "A permission that applies to the default resource type",
+            "type": "resource",
+            "logic": "POSITIVE",
+            "decisionStrategy": "UNANIMOUS",
+            "config": {
+              "defaultResourceType": "urn:gocloak:resources:default",
+              "applyPolicies": "[\"Default Policy\"]"
+            }
+          }
+        ],
+        "scopes": [],
+        "decisionStrategy": "UNANIMOUS"
+      }
     },
     {
       "id": "db4d47d3-31cd-49c7-a1c7-351e13da7907",
@@ -316,8 +382,8 @@
       "defaultClientScopes": [
         "web-origins",
         "role_list",
-        "profile",
         "roles",
+        "profile",
         "email"
       ],
       "optionalClientScopes": [
@@ -330,7 +396,8 @@
       "id": "0840c234-5b67-41f2-b001-415edf769bc3",
       "clientId": "account",
       "name": "${client_account}",
-      "baseUrl": "/auth/realms/gocloak/account",
+      "rootUrl": "${authBaseUrl}",
+      "baseUrl": "/realms/gocloak/account/",
       "surrogateAuthRequired": false,
       "enabled": true,
       "clientAuthenticatorType": "client-secret",
@@ -340,7 +407,7 @@
         "view-profile"
       ],
       "redirectUris": [
-        "/auth/realms/gocloak/account/*"
+        "/realms/gocloak/account/*"
       ],
       "webOrigins": [],
       "notBefore": 0,
@@ -360,14 +427,15 @@
       "defaultClientScopes": [
         "web-origins",
         "role_list",
-        "profile",
         "roles",
+        "profile",
         "email"
       ],
       "optionalClientScopes": [
         "address",
         "phone",
-        "offline_access"
+        "offline_access",
+        "microprofile-jwt"
       ]
     }
   ],
@@ -446,6 +514,48 @@
         }
       ]
     },
+    {
+      "id": "db6bf180-6218-4744-8c3a-068bd8804715",
+      "name": "microprofile-jwt",
+      "description": "Microprofile - JWT built-in scope",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "display.on.consent.screen": "false"
+      },
+      "protocolMappers": [
+        {
+          "id": "fb5df9d7-960b-4880-a66e-ef59d5ef374f",
+          "name": "groups",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-realm-role-mapper",
+          "consentRequired": false,
+          "config": {
+            "multivalued": "true",
+            "user.attribute": "foo",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "groups",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "9cd4d909-1cb1-4f25-bfcb-18400472fd4f",
+          "name": "upn",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-property-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "username",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "upn",
+            "jsonType.label": "String"
+          }
+        }
+      ]
+    },
     {
       "id": "20b53fea-ae28-484c-b241-2c3c98fc7715",
       "name": "offline_access",
@@ -719,30 +829,6 @@
         }
       ]
     },
-    {
-      "id": "fa4bfd0f-8dd6-411d-bd8c-6ec76792a33d",
-      "name": "role_list",
-      "description": "SAML role list",
-      "protocol": "saml",
-      "attributes": {
-        "consent.screen.text": "${samlRoleListScopeConsentText}",
-        "display.on.consent.screen": "true"
-      },
-      "protocolMappers": [
-        {
-          "id": "71fa5c48-aab3-4d59-bc85-5a33e4c98e60",
-          "name": "role list",
-          "protocol": "saml",
-          "protocolMapper": "saml-role-list-mapper",
-          "consentRequired": false,
-          "config": {
-            "single": "false",
-            "attribute.nameformat": "Basic",
-            "attribute.name": "Role"
-          }
-        }
-      ]
-    },
     {
       "id": "293d720c-02ad-4d3f-959d-8fe4b0cd464a",
       "name": "roles",
@@ -769,11 +855,11 @@
           "protocolMapper": "oidc-usermodel-realm-role-mapper",
           "consentRequired": false,
           "config": {
-            "multivalued": "true",
             "user.attribute": "foo",
             "access.token.claim": "true",
             "claim.name": "realm_access.roles",
-            "jsonType.label": "String"
+            "jsonType.label": "String",
+            "multivalued": "true"
           }
         },
         {
@@ -783,11 +869,35 @@
           "protocolMapper": "oidc-usermodel-client-role-mapper",
           "consentRequired": false,
           "config": {
-            "multivalued": "true",
             "user.attribute": "foo",
             "access.token.claim": "true",
             "claim.name": "resource_access.${client_id}.roles",
-            "jsonType.label": "String"
+            "jsonType.label": "String",
+            "multivalued": "true"
+          }
+        }
+      ]
+    },
+    {
+      "id": "fa4bfd0f-8dd6-411d-bd8c-6ec76792a33d",
+      "name": "role_list",
+      "description": "SAML role list",
+      "protocol": "saml",
+      "attributes": {
+        "consent.screen.text": "${samlRoleListScopeConsentText}",
+        "display.on.consent.screen": "true"
+      },
+      "protocolMappers": [
+        {
+          "id": "71fa5c48-aab3-4d59-bc85-5a33e4c98e60",
+          "name": "role list",
+          "protocol": "saml",
+          "protocolMapper": "saml-role-list-mapper",
+          "consentRequired": false,
+          "config": {
+            "single": "false",
+            "attribute.nameformat": "Basic",
+            "attribute.name": "Role"
           }
         }
       ]
@@ -815,24 +925,25 @@
     }
   ],
   "defaultDefaultClientScopes": [
-    "role_list",
-    "profile",
-    "email",
     "roles",
-    "web-origins"
+    "email",
+    "web-origins",
+    "profile",
+    "role_list"
   ],
   "defaultOptionalClientScopes": [
     "offline_access",
-    "address",
-    "phone"
+    "microprofile-jwt",
+    "phone",
+    "address"
   ],
   "browserSecurityHeaders": {
     "contentSecurityPolicyReportOnly": "",
     "xContentTypeOptions": "nosniff",
     "xRobotsTag": "none",
     "xFrameOptions": "SAMEORIGIN",
-    "xXSSProtection": "1; mode=block",
     "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
+    "xXSSProtection": "1; mode=block",
     "strictTransportSecurity": "max-age=31536000; includeSubDomains"
   },
   "smtpServer": {},
@@ -865,14 +976,14 @@
         "subComponents": {},
         "config": {
           "allowed-protocol-mapper-types": [
-            "oidc-usermodel-property-mapper",
-            "saml-role-list-mapper",
-            "saml-user-property-mapper",
             "oidc-full-name-mapper",
-            "oidc-address-mapper",
             "oidc-sha256-pairwise-sub-mapper",
-            "saml-user-attribute-mapper",
-            "oidc-usermodel-attribute-mapper"
+            "saml-user-property-mapper",
+            "oidc-address-mapper",
+            "saml-role-list-mapper",
+            "oidc-usermodel-property-mapper",
+            "oidc-usermodel-attribute-mapper",
+            "saml-user-attribute-mapper"
           ]
         }
       },
@@ -939,14 +1050,14 @@
         "subComponents": {},
         "config": {
           "allowed-protocol-mapper-types": [
-            "saml-user-attribute-mapper",
+            "saml-role-list-mapper",
+            "oidc-full-name-mapper",
             "saml-user-property-mapper",
-            "oidc-address-mapper",
             "oidc-usermodel-attribute-mapper",
-            "oidc-usermodel-property-mapper",
-            "oidc-full-name-mapper",
-            "saml-role-list-mapper",
-            "oidc-sha256-pairwise-sub-mapper"
+            "oidc-sha256-pairwise-sub-mapper",
+            "saml-user-attribute-mapper",
+            "oidc-address-mapper",
+            "oidc-usermodel-property-mapper"
           ]
         }
       }
@@ -994,7 +1105,7 @@
   "supportedLocales": [],
   "authenticationFlows": [
     {
-      "id": "bdd5149e-9654-45fa-b5c5-a4a5dac137e1",
+      "id": "f93eda62-488f-4090-acf1-9f3860f43afe",
       "alias": "Handle Existing Account",
       "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
       "providerId": "basic-flow",
@@ -1025,7 +1136,7 @@
       ]
     },
     {
-      "id": "65a9032b-584d-48b3-8621-cb83917a8c86",
+      "id": "2cf5c359-e97f-4d02-bcc4-f55208848035",
       "alias": "Verify Existing Account by Re-authentication",
       "description": "Reauthentication of existing account",
       "providerId": "basic-flow",
@@ -1039,9 +1150,33 @@
           "userSetupAllowed": false,
           "autheticatorFlow": false
         },
+        {
+          "requirement": "CONDITIONAL",
+          "priority": 20,
+          "flowAlias": "Verify Existing Account by Re-authentication - auth-otp-form - Conditional",
+          "userSetupAllowed": false,
+          "autheticatorFlow": true
+        }
+      ]
+    },
+    {
+      "id": "28c68909-9101-4e90-b0b4-85c26931aa68",
+      "alias": "Verify Existing Account by Re-authentication - auth-otp-form - Conditional",
+      "description": "Flow to determine if the auth-otp-form authenticator should be used or not.",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
         {
           "authenticator": "auth-otp-form",
-          "requirement": "OPTIONAL",
+          "requirement": "REQUIRED",
           "priority": 20,
           "userSetupAllowed": false,
           "autheticatorFlow": false
@@ -1049,7 +1184,7 @@
       ]
     },
     {
-      "id": "b2b55c00-6c8f-4bec-91c1-dfb4c4e603b5",
+      "id": "41ac5c4a-a8cf-4021-b520-7d663ba2fddf",
       "alias": "browser",
       "description": "browser based authentication",
       "providerId": "basic-flow",
@@ -1087,7 +1222,7 @@
       ]
     },
     {
-      "id": "ceb4c3c6-a022-4754-8440-dfa9ea2b85c2",
+      "id": "511338f4-4f87-4a89-a122-086ae71bc91b",
       "alias": "clients",
       "description": "Base authentication for clients",
       "providerId": "client-flow",
@@ -1125,7 +1260,7 @@
       ]
     },
     {
-      "id": "aea532b6-2c9a-4647-a3ff-ae1b0d915223",
+      "id": "e0ecc5e1-6a73-4455-b287-dd628e4a5818",
       "alias": "direct grant",
       "description": "OpenID Connect Resource Owner Grant",
       "providerId": "basic-flow",
@@ -1147,16 +1282,40 @@
           "autheticatorFlow": false
         },
         {
-          "authenticator": "direct-grant-validate-otp",
-          "requirement": "OPTIONAL",
+          "requirement": "CONDITIONAL",
           "priority": 30,
+          "flowAlias": "direct grant - direct-grant-validate-otp - Conditional",
+          "userSetupAllowed": false,
+          "autheticatorFlow": true
+        }
+      ]
+    },
+    {
+      "id": "790c1e45-2bd6-4b40-885d-610ee498d757",
+      "alias": "direct grant - direct-grant-validate-otp - Conditional",
+      "description": "Flow to determine if the direct-grant-validate-otp authenticator should be used or not.",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "direct-grant-validate-otp",
+          "requirement": "REQUIRED",
+          "priority": 20,
           "userSetupAllowed": false,
           "autheticatorFlow": false
         }
       ]
     },
     {
-      "id": "5b95aace-91a5-4f05-9843-9cfa39b81cf6",
+      "id": "99c79371-0894-46cd-8d7b-1ade136c6446",
       "alias": "docker auth",
       "description": "Used by Docker clients to authenticate against the IDP",
       "providerId": "basic-flow",
@@ -1173,7 +1332,7 @@
       ]
     },
     {
-      "id": "b78620cf-0dcc-43e5-ba5e-d0e0b19d3258",
+      "id": "8c133757-5745-4c3e-b408-43a3e6e00d51",
       "alias": "first broker login",
       "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
       "providerId": "basic-flow",
@@ -1206,7 +1365,7 @@
       ]
     },
     {
-      "id": "91e93867-56fd-4f61-b6a8-e31ba14364eb",
+      "id": "eb225469-846a-4125-b4cc-fa46e5dc4d19",
       "alias": "forms",
       "description": "Username, password, otp and other auth forms.",
       "providerId": "basic-flow",
@@ -1220,9 +1379,33 @@
           "userSetupAllowed": false,
           "autheticatorFlow": false
         },
+        {
+          "requirement": "CONDITIONAL",
+          "priority": 20,
+          "flowAlias": "forms - auth-otp-form - Conditional",
+          "userSetupAllowed": false,
+          "autheticatorFlow": true
+        }
+      ]
+    },
+    {
+      "id": "fa019a5c-d0e7-40d4-bbc8-2114749f451f",
+      "alias": "forms - auth-otp-form - Conditional",
+      "description": "Flow to determine if the auth-otp-form authenticator should be used or not.",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
         {
           "authenticator": "auth-otp-form",
-          "requirement": "OPTIONAL",
+          "requirement": "REQUIRED",
           "priority": 20,
           "userSetupAllowed": false,
           "autheticatorFlow": false
@@ -1230,7 +1413,7 @@
       ]
     },
     {
-      "id": "1a835c11-27e9-498d-8bd3-c548a978db85",
+      "id": "f60fc2cb-c80d-4927-ac2d-993217e5d828",
       "alias": "http challenge",
       "description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
       "providerId": "basic-flow",
@@ -1268,7 +1451,7 @@
       ]
     },
     {
-      "id": "dc94822f-8f49-46ee-882c-f3403e13190b",
+      "id": "7e9a515a-c42f-4352-9d2c-68260829d6b9",
       "alias": "registration",
       "description": "registration flow",
       "providerId": "basic-flow",
@@ -1286,7 +1469,7 @@
       ]
     },
     {
-      "id": "f911b16a-019b-41e1-82b2-f089310a1abf",
+      "id": "2a162eb4-830b-4719-b892-1b8748d75d77",
       "alias": "registration form",
       "description": "registration form",
       "providerId": "form-flow",
@@ -1324,7 +1507,7 @@
       ]
     },
     {
-      "id": "b3ca8141-7045-45f1-88ae-edaddf6fe228",
+      "id": "5105754f-69c6-4dc2-96ef-842b24ff8d19",
       "alias": "reset credentials",
       "description": "Reset credentials for a user if they forgot their password or something",
       "providerId": "basic-flow",
@@ -1353,16 +1536,40 @@
           "autheticatorFlow": false
         },
         {
-          "authenticator": "reset-otp",
-          "requirement": "OPTIONAL",
+          "requirement": "CONDITIONAL",
           "priority": 40,
+          "flowAlias": "reset credentials - reset-otp - Conditional",
+          "userSetupAllowed": false,
+          "autheticatorFlow": true
+        }
+      ]
+    },
+    {
+      "id": "2286d2e0-023e-4f34-bf34-fc1f137223bf",
+      "alias": "reset credentials - reset-otp - Conditional",
+      "description": "Flow to determine if the reset-otp authenticator should be used or not.",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "reset-otp",
+          "requirement": "REQUIRED",
+          "priority": 20,
           "userSetupAllowed": false,
           "autheticatorFlow": false
         }
       ]
     },
     {
-      "id": "088725ec-c2a6-481f-8c08-e79448b1420f",
+      "id": "b4465808-dfac-4af2-9f60-bcc362f70835",
       "alias": "saml ecp",
       "description": "SAML ECP Profile Authentication Flow",
       "providerId": "basic-flow",
@@ -1381,14 +1588,14 @@
   ],
   "authenticatorConfig": [
     {
-      "id": "4937663d-d746-4f0f-bd76-45f6df88edc2",
+      "id": "ae8a005d-8df1-4911-9289-7e507c6778d2",
       "alias": "create unique user config",
       "config": {
         "require.password.update.after.registration": "false"
       }
     },
     {
-      "id": "fed3f992-328d-4135-b658-e5e52dac8c49",
+      "id": "743328ea-7ce0-4e3e-9110-cec0687fc770",
       "alias": "review profile config",
       "config": {
         "update.profile.on.first.login": "missing"
@@ -1449,26 +1656,35 @@
   "clientAuthenticationFlow": "clients",
   "dockerAuthenticationFlow": "docker auth",
   "attributes": {
-    "_browser_header.xXSSProtection": "1; mode=block",
-    "_browser_header.xFrameOptions": "SAMEORIGIN",
-    "_browser_header.strictTransportSecurity": "max-age=31536000; includeSubDomains",
-    "permanentLockout": "false",
-    "quickLoginCheckMilliSeconds": "1000",
+    "webAuthnPolicyAuthenticatorAttachment": "not specified",
     "_browser_header.xRobotsTag": "none",
-    "maxFailureWaitSeconds": "900",
-    "minimumQuickLoginWaitSeconds": "60",
+    "webAuthnPolicyRpEntityName": "keycloak",
     "failureFactor": "30",
     "actionTokenGeneratedByUserLifespan": "300",
     "maxDeltaTimeSeconds": "43200",
-    "_browser_header.xContentTypeOptions": "nosniff",
+    "webAuthnPolicySignatureAlgorithms": "ES256",
     "offlineSessionMaxLifespan": "5184000",
-    "actionTokenGeneratedByAdminLifespan": "43200",
     "_browser_header.contentSecurityPolicyReportOnly": "",
     "bruteForceProtected": "false",
     "_browser_header.contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
-    "waitIncrementSeconds": "60",
-    "offlineSessionMaxLifespanEnabled": "false"
+    "_browser_header.xXSSProtection": "1; mode=block",
+    "webAuthnPolicyUserVerificationRequirement": "not specified",
+    "_browser_header.strictTransportSecurity": "max-age=31536000; includeSubDomains",
+    "_browser_header.xFrameOptions": "SAMEORIGIN",
+    "permanentLockout": "false",
+    "quickLoginCheckMilliSeconds": "1000",
+    "webAuthnPolicyCreateTimeout": "0",
+    "webAuthnPolicyRequireResidentKey": "not specified",
+    "webAuthnPolicyAttestationConveyancePreference": "not specified",
+    "webAuthnPolicyRpId": "",
+    "maxFailureWaitSeconds": "900",
+    "minimumQuickLoginWaitSeconds": "60",
+    "webAuthnPolicyAvoidSameAuthenticatorRegister": "false",
+    "_browser_header.xContentTypeOptions": "nosniff",
+    "actionTokenGeneratedByAdminLifespan": "43200",
+    "offlineSessionMaxLifespanEnabled": "false",
+    "waitIncrementSeconds": "60"
   },
-  "keycloakVersion": "4.8.1.Final",
+  "keycloakVersion": "8.0.1",
   "userManagedAccessAllowed": false
 }
\ No newline at end of file