├── .github ├── docs │ ├── config-auth-flow-override.png │ ├── config-authentication.png │ ├── config-client-login-theme.png │ ├── config-execution.png │ ├── login-form-error.png │ └── login-form.png └── workflows │ ├── maven.yml │ └── release-github-tag.yml ├── .gitignore ├── LICENSE ├── README.md ├── dev-realm.json ├── docker-compose.yml ├── pom.xml └── src └── main ├── java └── io │ └── github │ └── kilmajster │ └── keycloak │ ├── UsernamePasswordAttributeForm.java │ ├── UsernamePasswordAttributeFormConfiguration.java │ └── UsernamePasswordAttributeFormFactory.java └── resources ├── META-INF ├── keycloak-themes.json └── services │ └── org.keycloak.authentication.AuthenticatorFactory └── theme └── base-with-attribute └── login ├── login.ftl ├── messages └── messages_en.properties └── theme.properties /.github/docs/config-auth-flow-override.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilmajster/keycloak-username-password-attribute-authenticator/e8cdeac129b1116ee5cb3c101f8c22f4f43ad9ba/.github/docs/config-auth-flow-override.png -------------------------------------------------------------------------------- /.github/docs/config-authentication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilmajster/keycloak-username-password-attribute-authenticator/e8cdeac129b1116ee5cb3c101f8c22f4f43ad9ba/.github/docs/config-authentication.png -------------------------------------------------------------------------------- /.github/docs/config-client-login-theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilmajster/keycloak-username-password-attribute-authenticator/e8cdeac129b1116ee5cb3c101f8c22f4f43ad9ba/.github/docs/config-client-login-theme.png -------------------------------------------------------------------------------- /.github/docs/config-execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilmajster/keycloak-username-password-attribute-authenticator/e8cdeac129b1116ee5cb3c101f8c22f4f43ad9ba/.github/docs/config-execution.png -------------------------------------------------------------------------------- /.github/docs/login-form-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilmajster/keycloak-username-password-attribute-authenticator/e8cdeac129b1116ee5cb3c101f8c22f4f43ad9ba/.github/docs/login-form-error.png -------------------------------------------------------------------------------- /.github/docs/login-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilmajster/keycloak-username-password-attribute-authenticator/e8cdeac129b1116ee5cb3c101f8c22f4f43ad9ba/.github/docs/login-form.png -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | push: 4 | branches: [ main, development ] 5 | pull_request: 6 | branches: [ main ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Set up Maven 15 | uses: actions/setup-java@v4 16 | with: 17 | java-version: '17' 18 | distribution: 'adopt' 19 | 20 | - name: Build with Maven 21 | run: mvn -B -ntp package --file pom.xml -------------------------------------------------------------------------------- /.github/workflows/release-github-tag.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to the Maven Central Repository & Docker Hub 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release-github-tag: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - uses: actions/setup-java@v4 14 | with: 15 | distribution: 'adopt' 16 | java-version: '17' 17 | 18 | - name: Get the tag name 19 | run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 20 | 21 | - name: Set version from git tag 22 | run: mvn -B -ntp versions:set -DgenerateBackupPoms=false -DnewVersion="$TAG" 23 | 24 | - name: Build a project & run unit tests 25 | run: mvn -B -ntp package 26 | 27 | - name: Add jar to Github Release 28 | uses: svenstaro/upload-release-action@v2 29 | continue-on-error: true 30 | with: 31 | repo_token: ${{ secrets.GITHUB_TOKEN }} 32 | file: target/keycloak-username-password-attribute-authenticator-${{ env.TAG }}.jar 33 | asset_name: keycloak-username-password-attribute-authenticator-${{ env.TAG }}.jar 34 | tag: ${{ github.ref }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | /build/ 25 | /keycloak-username-password-attribute-authenticator.iml 26 | /.idea/ 27 | /target/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Łukasz Włódarczyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keycloak username password attribute authenticator 2 | [![main](https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/actions/workflows/maven.yml/badge.svg)](https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/actions/workflows/maven.yml) 3 | ![GitHub](https://img.shields.io/github/license/kilmajster/keycloak-username-password-attribute-authenticator) 4 | 5 | #### Supported Keycloak versions 6 | | compatible with Keycloak - 16.1.1 | [`keycloak-username-password-attribute-authenticator:0.3.0`](https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/tree/0.3.0) | 7 | |-------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| 8 | | compatible with Keycloak - 24.0.1 | [`keycloak-username-password-attribute-authenticator:1.0.1`](https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/tree/main) | 9 | 10 | ## Description 11 | Keycloak default login form with additional user attribute validation. Example: 12 | 13 |

14 | Login form preview 15 |     16 | Form error message preview 17 |

18 | 19 | ## Usage 20 | To use this authenticator, it should be bundled together with Keycloak, here's how do that: 21 | 22 | ### Deploying jar 23 | Build your Keycloak image like below: 24 | ```Dockerfile 25 | FROM quay.io/keycloak/keycloak:24.0.1 26 | 27 | RUN curl -s -L -o /opt/keycloak/providers/keycloak-username-password-attribute-authenticator-1.0.1.jar https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/releases/download/1.0.1/keycloak-username-password-attribute-authenticator-1.0.1.jar 28 | RUN /opt/keycloak/bin/kc.sh build 29 | 30 | ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start"] 31 | ``` 32 | 33 | ## Authentication configuration 34 | Following steps shows how to create authentication flow that uses authenticator with user attribute validation. 35 | 1. In Keycloak admin console, go to _Authentication_ section, select authentication type of _Browser_ and click 36 | _Duplicate_ action. 37 | 2. Set name for new authentication flow eg. `Browser with user attribute` and click _Ok_. 38 | 3. In newly created authentication flow remove _Username Password Form_ execution. 39 | 4. On _Browser With User Attribute Forms_ level, click _Actions_ > _Add execution_ and select provider of type 40 | _Username Password Attribute Form_, set _Requirement_ to `required`, then save. 41 | 5. Then move _Username Password Attribute Form_ on a previous position of _Username Password Form_, 42 | so in the end authentication flow should look like following: 43 |

44 | New authentication execution 45 |

46 | 6. On _Username Password Attribute Form_ level, click _Actions_ > _Settings_. 47 |

48 | Authenticator configuration 49 |

50 | 51 | ### Minimal configuration 52 | - ##### `User attribute` 53 | Attribute used to validate login form. 54 | ### Advanced configuration 55 | - ##### `Generate label` (default true) 56 | If enabled, label for login form will be generated based on attribute name, so attribute with name: 57 | - `favorite_number` will be labeled as _Favorite number_ 58 | - `REALLY_custom.user-Attribute` will be translated to _Really custom user attribute_, etc. 59 | By default, set to `true`. If `User attribute form label` 60 | is configured, label is taken form configuration and generation is skipped. 61 | - ##### `User attribute form label` 62 | Message which will be displayed as user attribute input label. If value is a valid message key, then proper translation will be used. 63 | - ##### `Invalid user attribute error message` 64 | Message which will be displayed as user attribute validation error. If value is a valid message key, then proper translation will be used. 65 | 66 | ## Theme configuration 67 | Theme configuration is handled in clients section, in following example Keycloak default `account-console` client will be used. 68 | 69 | ### Using bundled default Keycloak theme 70 | In Keycloak admin panel, go to _Clients_ and select client you want to authenticate with user attribute form. As _Login Theme_ set `base-with-attribute` 71 |

72 | Example client configuration 73 |

74 | Then in advance section > _Authentication Flow Overrides_ for _Browser Flow_, choose authentication that contain previously configured login form, 75 | so for example `Browser with user attribute`. 76 |

77 | Example client configuration 78 |

79 | 80 | 81 | ### Extending own theme 82 | If you have your own theme, then in `.your-theme/login/login.ftl` add following below `
` responsible for a password stuff or anywhere you want. 83 | How it was done with _Keycloak base_ theme, you can check [here](/src/main/resources/theme/base-with-attribute/login/login.ftl). 84 | ```html 85 | <#if usernameHidden?? && messagesPerField.existsError('username','password')> 86 | 87 | ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} 88 | 89 | 90 | 91 |
92 | 93 | 94 |
95 | 96 |
97 | 100 | 106 |
107 | <#if usernameHidden?? && messagesPerField.existsError('username','password', 'user_attribute')> 108 | 109 | ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} 110 | 111 | 112 |
113 | 114 | 115 |
116 |
117 | <#if realm.rememberMe && !usernameHidden??> 118 |
119 | ``` 120 | 121 | ### Testing & development 122 | #### Build the project 123 | ```shell 124 | $ mvn package 125 | ``` 126 | #### Run Keycloak with authenticator in docker compose 127 | After building a project, do following to start Keycloak with bundled authenticator jar and dummy configuration ([`dev-realm.json`](dev-realm.json)). 128 | ```shell 129 | $ docker compose up 130 | ``` 131 | Open browser and go to http://localhost:8080/realms/dev-realm/account 132 | use _Username or email_ = `test`, _Password_ = `test` and _Favorite number_ = `46` to login. 133 | -------------------------------------------------------------------------------- /dev-realm.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "834073ca-9a3c-4e36-aa47-f8866b247935", 3 | "realm": "dev-realm", 4 | "notBefore": 0, 5 | "defaultSignatureAlgorithm": "RS256", 6 | "revokeRefreshToken": false, 7 | "refreshTokenMaxReuse": 0, 8 | "accessTokenLifespan": 300, 9 | "accessTokenLifespanForImplicitFlow": 900, 10 | "ssoSessionIdleTimeout": 1800, 11 | "ssoSessionMaxLifespan": 36000, 12 | "ssoSessionIdleTimeoutRememberMe": 0, 13 | "ssoSessionMaxLifespanRememberMe": 0, 14 | "offlineSessionIdleTimeout": 2592000, 15 | "offlineSessionMaxLifespanEnabled": false, 16 | "offlineSessionMaxLifespan": 5184000, 17 | "clientSessionIdleTimeout": 0, 18 | "clientSessionMaxLifespan": 0, 19 | "clientOfflineSessionIdleTimeout": 0, 20 | "clientOfflineSessionMaxLifespan": 0, 21 | "accessCodeLifespan": 60, 22 | "accessCodeLifespanUserAction": 300, 23 | "accessCodeLifespanLogin": 1800, 24 | "actionTokenGeneratedByAdminLifespan": 43200, 25 | "actionTokenGeneratedByUserLifespan": 300, 26 | "oauth2DeviceCodeLifespan": 600, 27 | "oauth2DevicePollingInterval": 5, 28 | "enabled": true, 29 | "sslRequired": "external", 30 | "registrationAllowed": false, 31 | "registrationEmailAsUsername": false, 32 | "rememberMe": false, 33 | "verifyEmail": false, 34 | "loginWithEmailAllowed": true, 35 | "duplicateEmailsAllowed": false, 36 | "resetPasswordAllowed": false, 37 | "editUsernameAllowed": false, 38 | "bruteForceProtected": false, 39 | "permanentLockout": false, 40 | "maxFailureWaitSeconds": 900, 41 | "minimumQuickLoginWaitSeconds": 60, 42 | "waitIncrementSeconds": 60, 43 | "quickLoginCheckMilliSeconds": 1000, 44 | "maxDeltaTimeSeconds": 43200, 45 | "failureFactor": 30, 46 | "defaultRole": { 47 | "id": "2fe40b1b-b89a-402a-8ce6-566a78268bc4", 48 | "name": "default-roles-dev-realm", 49 | "description": "${role_default-roles}", 50 | "composite": true, 51 | "clientRole": false, 52 | "containerId": "834073ca-9a3c-4e36-aa47-f8866b247935" 53 | }, 54 | "requiredCredentials": [ 55 | "password" 56 | ], 57 | "otpPolicyType": "totp", 58 | "otpPolicyAlgorithm": "HmacSHA1", 59 | "otpPolicyInitialCounter": 0, 60 | "otpPolicyDigits": 6, 61 | "otpPolicyLookAheadWindow": 1, 62 | "otpPolicyPeriod": 30, 63 | "otpPolicyCodeReusable": false, 64 | "otpSupportedApplications": [ 65 | "totpAppMicrosoftAuthenticatorName", 66 | "totpAppFreeOTPName", 67 | "totpAppGoogleName" 68 | ], 69 | "webAuthnPolicyRpEntityName": "keycloak", 70 | "webAuthnPolicySignatureAlgorithms": [ 71 | "ES256" 72 | ], 73 | "webAuthnPolicyRpId": "", 74 | "webAuthnPolicyAttestationConveyancePreference": "not specified", 75 | "webAuthnPolicyAuthenticatorAttachment": "not specified", 76 | "webAuthnPolicyRequireResidentKey": "not specified", 77 | "webAuthnPolicyUserVerificationRequirement": "not specified", 78 | "webAuthnPolicyCreateTimeout": 0, 79 | "webAuthnPolicyAvoidSameAuthenticatorRegister": false, 80 | "webAuthnPolicyAcceptableAaguids": [], 81 | "webAuthnPolicyPasswordlessRpEntityName": "keycloak", 82 | "webAuthnPolicyPasswordlessSignatureAlgorithms": [ 83 | "ES256" 84 | ], 85 | "webAuthnPolicyPasswordlessRpId": "", 86 | "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", 87 | "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", 88 | "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", 89 | "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", 90 | "webAuthnPolicyPasswordlessCreateTimeout": 0, 91 | "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, 92 | "webAuthnPolicyPasswordlessAcceptableAaguids": [], 93 | "scopeMappings": [ 94 | { 95 | "clientScope": "offline_access", 96 | "roles": [ 97 | "offline_access" 98 | ] 99 | } 100 | ], 101 | "clientScopeMappings": { 102 | "account": [ 103 | { 104 | "client": "account-console", 105 | "roles": [ 106 | "manage-account", 107 | "view-groups" 108 | ] 109 | } 110 | ] 111 | }, 112 | "clients": [ 113 | { 114 | "id": "6c84289f-d0ea-4651-b6a9-dc9cb06ce9a8", 115 | "clientId": "account", 116 | "name": "${client_account}", 117 | "rootUrl": "${authBaseUrl}", 118 | "baseUrl": "/realms/dev-realm/account/", 119 | "surrogateAuthRequired": false, 120 | "enabled": true, 121 | "alwaysDisplayInConsole": false, 122 | "clientAuthenticatorType": "client-secret", 123 | "redirectUris": [ 124 | "/realms/dev-realm/account/*" 125 | ], 126 | "webOrigins": [], 127 | "notBefore": 0, 128 | "bearerOnly": false, 129 | "consentRequired": false, 130 | "standardFlowEnabled": true, 131 | "implicitFlowEnabled": false, 132 | "directAccessGrantsEnabled": false, 133 | "serviceAccountsEnabled": false, 134 | "publicClient": true, 135 | "frontchannelLogout": false, 136 | "protocol": "openid-connect", 137 | "attributes": { 138 | "post.logout.redirect.uris": "+" 139 | }, 140 | "authenticationFlowBindingOverrides": {}, 141 | "fullScopeAllowed": false, 142 | "nodeReRegistrationTimeout": 0, 143 | "defaultClientScopes": [ 144 | "web-origins", 145 | "acr", 146 | "roles", 147 | "profile", 148 | "email" 149 | ], 150 | "optionalClientScopes": [ 151 | "address", 152 | "phone", 153 | "offline_access", 154 | "microprofile-jwt" 155 | ] 156 | }, 157 | { 158 | "id": "1a60274d-756d-4351-8c5e-a41769834c49", 159 | "clientId": "account-console", 160 | "name": "${client_account-console}", 161 | "description": "", 162 | "rootUrl": "${authBaseUrl}", 163 | "adminUrl": "", 164 | "baseUrl": "/realms/dev-realm/account/", 165 | "surrogateAuthRequired": false, 166 | "enabled": true, 167 | "alwaysDisplayInConsole": false, 168 | "clientAuthenticatorType": "client-secret", 169 | "redirectUris": [ 170 | "/realms/dev-realm/account/*" 171 | ], 172 | "webOrigins": [], 173 | "notBefore": 0, 174 | "bearerOnly": false, 175 | "consentRequired": false, 176 | "standardFlowEnabled": true, 177 | "implicitFlowEnabled": false, 178 | "directAccessGrantsEnabled": false, 179 | "serviceAccountsEnabled": false, 180 | "publicClient": true, 181 | "frontchannelLogout": false, 182 | "protocol": "openid-connect", 183 | "attributes": { 184 | "login_theme": "base-with-attribute", 185 | "post.logout.redirect.uris": "+", 186 | "oauth2.device.authorization.grant.enabled": "false", 187 | "backchannel.logout.revoke.offline.tokens": "false", 188 | "use.refresh.tokens": "true", 189 | "oidc.ciba.grant.enabled": "false", 190 | "backchannel.logout.session.required": "true", 191 | "client_credentials.use_refresh_token": "false", 192 | "tls.client.certificate.bound.access.tokens": "false", 193 | "require.pushed.authorization.requests": "false", 194 | "acr.loa.map": "{}", 195 | "display.on.consent.screen": "false", 196 | "pkce.code.challenge.method": "S256", 197 | "token.response.type.bearer.lower-case": "false" 198 | }, 199 | "authenticationFlowBindingOverrides": { 200 | "browser": "f67cafc8-d118-4824-9cc5-a704f6e53d32" 201 | }, 202 | "fullScopeAllowed": false, 203 | "nodeReRegistrationTimeout": 0, 204 | "protocolMappers": [ 205 | { 206 | "id": "c248feec-49a5-49ab-b2c1-c513cc55731c", 207 | "name": "audience resolve", 208 | "protocol": "openid-connect", 209 | "protocolMapper": "oidc-audience-resolve-mapper", 210 | "consentRequired": false, 211 | "config": {} 212 | } 213 | ], 214 | "defaultClientScopes": [ 215 | "web-origins", 216 | "acr", 217 | "roles", 218 | "profile", 219 | "email" 220 | ], 221 | "optionalClientScopes": [ 222 | "address", 223 | "phone", 224 | "offline_access", 225 | "microprofile-jwt" 226 | ] 227 | }, 228 | { 229 | "id": "ca55cdf2-5e24-473d-9ed3-7d3415638d65", 230 | "clientId": "admin-cli", 231 | "name": "${client_admin-cli}", 232 | "surrogateAuthRequired": false, 233 | "enabled": true, 234 | "alwaysDisplayInConsole": false, 235 | "clientAuthenticatorType": "client-secret", 236 | "redirectUris": [], 237 | "webOrigins": [], 238 | "notBefore": 0, 239 | "bearerOnly": false, 240 | "consentRequired": false, 241 | "standardFlowEnabled": false, 242 | "implicitFlowEnabled": false, 243 | "directAccessGrantsEnabled": true, 244 | "serviceAccountsEnabled": false, 245 | "publicClient": true, 246 | "frontchannelLogout": false, 247 | "protocol": "openid-connect", 248 | "attributes": {}, 249 | "authenticationFlowBindingOverrides": {}, 250 | "fullScopeAllowed": false, 251 | "nodeReRegistrationTimeout": 0, 252 | "defaultClientScopes": [ 253 | "web-origins", 254 | "acr", 255 | "roles", 256 | "profile", 257 | "email" 258 | ], 259 | "optionalClientScopes": [ 260 | "address", 261 | "phone", 262 | "offline_access", 263 | "microprofile-jwt" 264 | ] 265 | }, 266 | { 267 | "id": "a52ee408-0952-4913-b572-d8a343e1684b", 268 | "clientId": "broker", 269 | "name": "${client_broker}", 270 | "surrogateAuthRequired": false, 271 | "enabled": true, 272 | "alwaysDisplayInConsole": false, 273 | "clientAuthenticatorType": "client-secret", 274 | "redirectUris": [], 275 | "webOrigins": [], 276 | "notBefore": 0, 277 | "bearerOnly": true, 278 | "consentRequired": false, 279 | "standardFlowEnabled": true, 280 | "implicitFlowEnabled": false, 281 | "directAccessGrantsEnabled": false, 282 | "serviceAccountsEnabled": false, 283 | "publicClient": false, 284 | "frontchannelLogout": false, 285 | "protocol": "openid-connect", 286 | "attributes": {}, 287 | "authenticationFlowBindingOverrides": {}, 288 | "fullScopeAllowed": false, 289 | "nodeReRegistrationTimeout": 0, 290 | "defaultClientScopes": [ 291 | "web-origins", 292 | "acr", 293 | "roles", 294 | "profile", 295 | "email" 296 | ], 297 | "optionalClientScopes": [ 298 | "address", 299 | "phone", 300 | "offline_access", 301 | "microprofile-jwt" 302 | ] 303 | }, 304 | { 305 | "id": "4db9b7d3-fb96-4b14-a8cf-b2f75525f5c1", 306 | "clientId": "realm-management", 307 | "name": "${client_realm-management}", 308 | "surrogateAuthRequired": false, 309 | "enabled": true, 310 | "alwaysDisplayInConsole": false, 311 | "clientAuthenticatorType": "client-secret", 312 | "redirectUris": [], 313 | "webOrigins": [], 314 | "notBefore": 0, 315 | "bearerOnly": true, 316 | "consentRequired": false, 317 | "standardFlowEnabled": true, 318 | "implicitFlowEnabled": false, 319 | "directAccessGrantsEnabled": false, 320 | "serviceAccountsEnabled": false, 321 | "publicClient": false, 322 | "frontchannelLogout": false, 323 | "protocol": "openid-connect", 324 | "attributes": {}, 325 | "authenticationFlowBindingOverrides": {}, 326 | "fullScopeAllowed": false, 327 | "nodeReRegistrationTimeout": 0, 328 | "defaultClientScopes": [ 329 | "web-origins", 330 | "acr", 331 | "roles", 332 | "profile", 333 | "email" 334 | ], 335 | "optionalClientScopes": [ 336 | "address", 337 | "phone", 338 | "offline_access", 339 | "microprofile-jwt" 340 | ] 341 | }, 342 | { 343 | "id": "6037cb4f-e522-4916-b2ef-18bbe37398cf", 344 | "clientId": "security-admin-console", 345 | "name": "${client_security-admin-console}", 346 | "rootUrl": "${authAdminUrl}", 347 | "baseUrl": "/admin/dev-realm/console/", 348 | "surrogateAuthRequired": false, 349 | "enabled": true, 350 | "alwaysDisplayInConsole": false, 351 | "clientAuthenticatorType": "client-secret", 352 | "redirectUris": [ 353 | "/admin/dev-realm/console/*" 354 | ], 355 | "webOrigins": [ 356 | "+" 357 | ], 358 | "notBefore": 0, 359 | "bearerOnly": false, 360 | "consentRequired": false, 361 | "standardFlowEnabled": true, 362 | "implicitFlowEnabled": false, 363 | "directAccessGrantsEnabled": false, 364 | "serviceAccountsEnabled": false, 365 | "publicClient": true, 366 | "frontchannelLogout": false, 367 | "protocol": "openid-connect", 368 | "attributes": { 369 | "post.logout.redirect.uris": "+", 370 | "pkce.code.challenge.method": "S256" 371 | }, 372 | "authenticationFlowBindingOverrides": {}, 373 | "fullScopeAllowed": false, 374 | "nodeReRegistrationTimeout": 0, 375 | "protocolMappers": [ 376 | { 377 | "id": "22ba440a-238f-4786-9f14-d0243b093365", 378 | "name": "locale", 379 | "protocol": "openid-connect", 380 | "protocolMapper": "oidc-usermodel-attribute-mapper", 381 | "consentRequired": false, 382 | "config": { 383 | "userinfo.token.claim": "true", 384 | "user.attribute": "locale", 385 | "id.token.claim": "true", 386 | "access.token.claim": "true", 387 | "claim.name": "locale", 388 | "jsonType.label": "String" 389 | } 390 | } 391 | ], 392 | "defaultClientScopes": [ 393 | "web-origins", 394 | "acr", 395 | "roles", 396 | "profile", 397 | "email" 398 | ], 399 | "optionalClientScopes": [ 400 | "address", 401 | "phone", 402 | "offline_access", 403 | "microprofile-jwt" 404 | ] 405 | } 406 | ], 407 | "clientScopes": [ 408 | { 409 | "id": "e1340e1d-18db-4b28-8d95-01b0efe85648", 410 | "name": "email", 411 | "description": "OpenID Connect built-in scope: email", 412 | "protocol": "openid-connect", 413 | "attributes": { 414 | "include.in.token.scope": "true", 415 | "display.on.consent.screen": "true", 416 | "consent.screen.text": "${emailScopeConsentText}" 417 | }, 418 | "protocolMappers": [ 419 | { 420 | "id": "731ebd0c-60fd-4334-82f5-c6b655bea46a", 421 | "name": "email verified", 422 | "protocol": "openid-connect", 423 | "protocolMapper": "oidc-usermodel-property-mapper", 424 | "consentRequired": false, 425 | "config": { 426 | "userinfo.token.claim": "true", 427 | "user.attribute": "emailVerified", 428 | "id.token.claim": "true", 429 | "access.token.claim": "true", 430 | "claim.name": "email_verified", 431 | "jsonType.label": "boolean" 432 | } 433 | }, 434 | { 435 | "id": "8fff12dd-e855-4bdb-96c5-fa22d1f3c4b1", 436 | "name": "email", 437 | "protocol": "openid-connect", 438 | "protocolMapper": "oidc-usermodel-attribute-mapper", 439 | "consentRequired": false, 440 | "config": { 441 | "userinfo.token.claim": "true", 442 | "user.attribute": "email", 443 | "id.token.claim": "true", 444 | "access.token.claim": "true", 445 | "claim.name": "email", 446 | "jsonType.label": "String" 447 | } 448 | } 449 | ] 450 | }, 451 | { 452 | "id": "fa673ddd-a855-4590-8d4f-ab13f7e247af", 453 | "name": "acr", 454 | "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", 455 | "protocol": "openid-connect", 456 | "attributes": { 457 | "include.in.token.scope": "false", 458 | "display.on.consent.screen": "false" 459 | }, 460 | "protocolMappers": [ 461 | { 462 | "id": "f7e82b99-268a-4c4c-a7eb-bb62e578081d", 463 | "name": "acr loa level", 464 | "protocol": "openid-connect", 465 | "protocolMapper": "oidc-acr-mapper", 466 | "consentRequired": false, 467 | "config": { 468 | "id.token.claim": "true", 469 | "access.token.claim": "true" 470 | } 471 | } 472 | ] 473 | }, 474 | { 475 | "id": "dd2cc4ab-9ad3-40f5-b874-6e3c9e0caefa", 476 | "name": "address", 477 | "description": "OpenID Connect built-in scope: address", 478 | "protocol": "openid-connect", 479 | "attributes": { 480 | "include.in.token.scope": "true", 481 | "display.on.consent.screen": "true", 482 | "consent.screen.text": "${addressScopeConsentText}" 483 | }, 484 | "protocolMappers": [ 485 | { 486 | "id": "7df1b49b-7640-4e1e-85b6-370557dc7b16", 487 | "name": "address", 488 | "protocol": "openid-connect", 489 | "protocolMapper": "oidc-address-mapper", 490 | "consentRequired": false, 491 | "config": { 492 | "user.attribute.formatted": "formatted", 493 | "user.attribute.country": "country", 494 | "user.attribute.postal_code": "postal_code", 495 | "userinfo.token.claim": "true", 496 | "user.attribute.street": "street", 497 | "id.token.claim": "true", 498 | "user.attribute.region": "region", 499 | "access.token.claim": "true", 500 | "user.attribute.locality": "locality" 501 | } 502 | } 503 | ] 504 | }, 505 | { 506 | "id": "5419107b-28fd-442a-a4da-42f3a6ffdc54", 507 | "name": "role_list", 508 | "description": "SAML role list", 509 | "protocol": "saml", 510 | "attributes": { 511 | "consent.screen.text": "${samlRoleListScopeConsentText}", 512 | "display.on.consent.screen": "true" 513 | }, 514 | "protocolMappers": [ 515 | { 516 | "id": "5fe1d7c5-25c4-49a0-8821-ba0408f17570", 517 | "name": "role list", 518 | "protocol": "saml", 519 | "protocolMapper": "saml-role-list-mapper", 520 | "consentRequired": false, 521 | "config": { 522 | "single": "false", 523 | "attribute.nameformat": "Basic", 524 | "attribute.name": "Role" 525 | } 526 | } 527 | ] 528 | }, 529 | { 530 | "id": "27824e4d-de85-48d1-a942-d427a7c6584b", 531 | "name": "web-origins", 532 | "description": "OpenID Connect scope for add allowed web origins to the access token", 533 | "protocol": "openid-connect", 534 | "attributes": { 535 | "include.in.token.scope": "false", 536 | "display.on.consent.screen": "false", 537 | "consent.screen.text": "" 538 | }, 539 | "protocolMappers": [ 540 | { 541 | "id": "dccadfa1-3772-492f-b365-1992efc50273", 542 | "name": "allowed web origins", 543 | "protocol": "openid-connect", 544 | "protocolMapper": "oidc-allowed-origins-mapper", 545 | "consentRequired": false, 546 | "config": {} 547 | } 548 | ] 549 | }, 550 | { 551 | "id": "c82c6fcb-a954-4f73-8fea-9c882f26f8a1", 552 | "name": "offline_access", 553 | "description": "OpenID Connect built-in scope: offline_access", 554 | "protocol": "openid-connect", 555 | "attributes": { 556 | "consent.screen.text": "${offlineAccessScopeConsentText}", 557 | "display.on.consent.screen": "true" 558 | } 559 | }, 560 | { 561 | "id": "5d2bc3ff-cb61-4df7-bef3-542fc524e84e", 562 | "name": "phone", 563 | "description": "OpenID Connect built-in scope: phone", 564 | "protocol": "openid-connect", 565 | "attributes": { 566 | "include.in.token.scope": "true", 567 | "display.on.consent.screen": "true", 568 | "consent.screen.text": "${phoneScopeConsentText}" 569 | }, 570 | "protocolMappers": [ 571 | { 572 | "id": "066eab59-34cb-466d-84ef-ace79cf38d53", 573 | "name": "phone number", 574 | "protocol": "openid-connect", 575 | "protocolMapper": "oidc-usermodel-attribute-mapper", 576 | "consentRequired": false, 577 | "config": { 578 | "userinfo.token.claim": "true", 579 | "user.attribute": "phoneNumber", 580 | "id.token.claim": "true", 581 | "access.token.claim": "true", 582 | "claim.name": "phone_number", 583 | "jsonType.label": "String" 584 | } 585 | }, 586 | { 587 | "id": "c56590c7-8045-4f64-83d3-13eee3067892", 588 | "name": "phone number verified", 589 | "protocol": "openid-connect", 590 | "protocolMapper": "oidc-usermodel-attribute-mapper", 591 | "consentRequired": false, 592 | "config": { 593 | "userinfo.token.claim": "true", 594 | "user.attribute": "phoneNumberVerified", 595 | "id.token.claim": "true", 596 | "access.token.claim": "true", 597 | "claim.name": "phone_number_verified", 598 | "jsonType.label": "boolean" 599 | } 600 | } 601 | ] 602 | }, 603 | { 604 | "id": "a4d1d6ec-51ca-4bf1-b15b-53ad31b968d3", 605 | "name": "roles", 606 | "description": "OpenID Connect scope for add user roles to the access token", 607 | "protocol": "openid-connect", 608 | "attributes": { 609 | "include.in.token.scope": "false", 610 | "display.on.consent.screen": "true", 611 | "consent.screen.text": "${rolesScopeConsentText}" 612 | }, 613 | "protocolMappers": [ 614 | { 615 | "id": "94f73528-55ee-42e4-b62d-665ea7d8f637", 616 | "name": "realm roles", 617 | "protocol": "openid-connect", 618 | "protocolMapper": "oidc-usermodel-realm-role-mapper", 619 | "consentRequired": false, 620 | "config": { 621 | "user.attribute": "foo", 622 | "access.token.claim": "true", 623 | "claim.name": "realm_access.roles", 624 | "jsonType.label": "String", 625 | "multivalued": "true" 626 | } 627 | }, 628 | { 629 | "id": "64d400e9-4ca9-48c2-b3b7-98048be0ca08", 630 | "name": "audience resolve", 631 | "protocol": "openid-connect", 632 | "protocolMapper": "oidc-audience-resolve-mapper", 633 | "consentRequired": false, 634 | "config": {} 635 | }, 636 | { 637 | "id": "869f0374-0349-43c8-82a1-7fccb90e0027", 638 | "name": "client roles", 639 | "protocol": "openid-connect", 640 | "protocolMapper": "oidc-usermodel-client-role-mapper", 641 | "consentRequired": false, 642 | "config": { 643 | "user.attribute": "foo", 644 | "access.token.claim": "true", 645 | "claim.name": "resource_access.${client_id}.roles", 646 | "jsonType.label": "String", 647 | "multivalued": "true" 648 | } 649 | } 650 | ] 651 | }, 652 | { 653 | "id": "c1e4540f-6360-4f6f-8cf5-f227c82e2347", 654 | "name": "microprofile-jwt", 655 | "description": "Microprofile - JWT built-in scope", 656 | "protocol": "openid-connect", 657 | "attributes": { 658 | "include.in.token.scope": "true", 659 | "display.on.consent.screen": "false" 660 | }, 661 | "protocolMappers": [ 662 | { 663 | "id": "f65c9ad2-d1dd-4e4a-befb-10805f26f331", 664 | "name": "groups", 665 | "protocol": "openid-connect", 666 | "protocolMapper": "oidc-usermodel-realm-role-mapper", 667 | "consentRequired": false, 668 | "config": { 669 | "multivalued": "true", 670 | "user.attribute": "foo", 671 | "id.token.claim": "true", 672 | "access.token.claim": "true", 673 | "claim.name": "groups", 674 | "jsonType.label": "String" 675 | } 676 | }, 677 | { 678 | "id": "dbc22bba-3e86-4558-9179-06929f870065", 679 | "name": "upn", 680 | "protocol": "openid-connect", 681 | "protocolMapper": "oidc-usermodel-attribute-mapper", 682 | "consentRequired": false, 683 | "config": { 684 | "userinfo.token.claim": "true", 685 | "user.attribute": "username", 686 | "id.token.claim": "true", 687 | "access.token.claim": "true", 688 | "claim.name": "upn", 689 | "jsonType.label": "String" 690 | } 691 | } 692 | ] 693 | }, 694 | { 695 | "id": "e31c4c42-d263-44ad-b999-41578169c0dc", 696 | "name": "profile", 697 | "description": "OpenID Connect built-in scope: profile", 698 | "protocol": "openid-connect", 699 | "attributes": { 700 | "include.in.token.scope": "true", 701 | "display.on.consent.screen": "true", 702 | "consent.screen.text": "${profileScopeConsentText}" 703 | }, 704 | "protocolMappers": [ 705 | { 706 | "id": "68c442cd-ad18-46f9-9d1b-5a58606b6bdf", 707 | "name": "profile", 708 | "protocol": "openid-connect", 709 | "protocolMapper": "oidc-usermodel-attribute-mapper", 710 | "consentRequired": false, 711 | "config": { 712 | "userinfo.token.claim": "true", 713 | "user.attribute": "profile", 714 | "id.token.claim": "true", 715 | "access.token.claim": "true", 716 | "claim.name": "profile", 717 | "jsonType.label": "String" 718 | } 719 | }, 720 | { 721 | "id": "4e60bf75-e2fb-43ed-ad9c-a90e4a58f6a0", 722 | "name": "website", 723 | "protocol": "openid-connect", 724 | "protocolMapper": "oidc-usermodel-attribute-mapper", 725 | "consentRequired": false, 726 | "config": { 727 | "userinfo.token.claim": "true", 728 | "user.attribute": "website", 729 | "id.token.claim": "true", 730 | "access.token.claim": "true", 731 | "claim.name": "website", 732 | "jsonType.label": "String" 733 | } 734 | }, 735 | { 736 | "id": "47a09725-3bf9-427a-8196-adfe4e3d3551", 737 | "name": "full name", 738 | "protocol": "openid-connect", 739 | "protocolMapper": "oidc-full-name-mapper", 740 | "consentRequired": false, 741 | "config": { 742 | "id.token.claim": "true", 743 | "access.token.claim": "true", 744 | "userinfo.token.claim": "true" 745 | } 746 | }, 747 | { 748 | "id": "9a6f2d9c-9ead-488b-91db-8766e4e7d7d0", 749 | "name": "middle name", 750 | "protocol": "openid-connect", 751 | "protocolMapper": "oidc-usermodel-attribute-mapper", 752 | "consentRequired": false, 753 | "config": { 754 | "userinfo.token.claim": "true", 755 | "user.attribute": "middleName", 756 | "id.token.claim": "true", 757 | "access.token.claim": "true", 758 | "claim.name": "middle_name", 759 | "jsonType.label": "String" 760 | } 761 | }, 762 | { 763 | "id": "e2092e09-72a9-4b6d-8a5d-d81b24524d04", 764 | "name": "gender", 765 | "protocol": "openid-connect", 766 | "protocolMapper": "oidc-usermodel-attribute-mapper", 767 | "consentRequired": false, 768 | "config": { 769 | "userinfo.token.claim": "true", 770 | "user.attribute": "gender", 771 | "id.token.claim": "true", 772 | "access.token.claim": "true", 773 | "claim.name": "gender", 774 | "jsonType.label": "String" 775 | } 776 | }, 777 | { 778 | "id": "01814e18-7d30-4420-9efe-dd7b403e6e8c", 779 | "name": "family name", 780 | "protocol": "openid-connect", 781 | "protocolMapper": "oidc-usermodel-attribute-mapper", 782 | "consentRequired": false, 783 | "config": { 784 | "userinfo.token.claim": "true", 785 | "user.attribute": "lastName", 786 | "id.token.claim": "true", 787 | "access.token.claim": "true", 788 | "claim.name": "family_name", 789 | "jsonType.label": "String" 790 | } 791 | }, 792 | { 793 | "id": "9fff3b34-cee7-400c-9282-507262a9ac0e", 794 | "name": "given name", 795 | "protocol": "openid-connect", 796 | "protocolMapper": "oidc-usermodel-attribute-mapper", 797 | "consentRequired": false, 798 | "config": { 799 | "userinfo.token.claim": "true", 800 | "user.attribute": "firstName", 801 | "id.token.claim": "true", 802 | "access.token.claim": "true", 803 | "claim.name": "given_name", 804 | "jsonType.label": "String" 805 | } 806 | }, 807 | { 808 | "id": "c668358d-6b1b-433e-a1fe-1da138aac267", 809 | "name": "birthdate", 810 | "protocol": "openid-connect", 811 | "protocolMapper": "oidc-usermodel-attribute-mapper", 812 | "consentRequired": false, 813 | "config": { 814 | "userinfo.token.claim": "true", 815 | "user.attribute": "birthdate", 816 | "id.token.claim": "true", 817 | "access.token.claim": "true", 818 | "claim.name": "birthdate", 819 | "jsonType.label": "String" 820 | } 821 | }, 822 | { 823 | "id": "0e5d8488-9799-4370-8176-0b7c6883f408", 824 | "name": "picture", 825 | "protocol": "openid-connect", 826 | "protocolMapper": "oidc-usermodel-attribute-mapper", 827 | "consentRequired": false, 828 | "config": { 829 | "userinfo.token.claim": "true", 830 | "user.attribute": "picture", 831 | "id.token.claim": "true", 832 | "access.token.claim": "true", 833 | "claim.name": "picture", 834 | "jsonType.label": "String" 835 | } 836 | }, 837 | { 838 | "id": "912efed6-058b-4873-a7e0-b051976f0610", 839 | "name": "username", 840 | "protocol": "openid-connect", 841 | "protocolMapper": "oidc-usermodel-attribute-mapper", 842 | "consentRequired": false, 843 | "config": { 844 | "userinfo.token.claim": "true", 845 | "user.attribute": "username", 846 | "id.token.claim": "true", 847 | "access.token.claim": "true", 848 | "claim.name": "preferred_username", 849 | "jsonType.label": "String" 850 | } 851 | }, 852 | { 853 | "id": "4237436e-cab0-4e23-8cf3-197809517d29", 854 | "name": "zoneinfo", 855 | "protocol": "openid-connect", 856 | "protocolMapper": "oidc-usermodel-attribute-mapper", 857 | "consentRequired": false, 858 | "config": { 859 | "userinfo.token.claim": "true", 860 | "user.attribute": "zoneinfo", 861 | "id.token.claim": "true", 862 | "access.token.claim": "true", 863 | "claim.name": "zoneinfo", 864 | "jsonType.label": "String" 865 | } 866 | }, 867 | { 868 | "id": "763004da-39cd-4fe1-a739-027366e5c24e", 869 | "name": "locale", 870 | "protocol": "openid-connect", 871 | "protocolMapper": "oidc-usermodel-attribute-mapper", 872 | "consentRequired": false, 873 | "config": { 874 | "userinfo.token.claim": "true", 875 | "user.attribute": "locale", 876 | "id.token.claim": "true", 877 | "access.token.claim": "true", 878 | "claim.name": "locale", 879 | "jsonType.label": "String" 880 | } 881 | }, 882 | { 883 | "id": "fdc6c54b-8b7a-496d-9292-14cf8a24aa66", 884 | "name": "nickname", 885 | "protocol": "openid-connect", 886 | "protocolMapper": "oidc-usermodel-attribute-mapper", 887 | "consentRequired": false, 888 | "config": { 889 | "userinfo.token.claim": "true", 890 | "user.attribute": "nickname", 891 | "id.token.claim": "true", 892 | "access.token.claim": "true", 893 | "claim.name": "nickname", 894 | "jsonType.label": "String" 895 | } 896 | }, 897 | { 898 | "id": "0bab3185-3fe0-4efe-b29f-26acebe4b08a", 899 | "name": "updated at", 900 | "protocol": "openid-connect", 901 | "protocolMapper": "oidc-usermodel-attribute-mapper", 902 | "consentRequired": false, 903 | "config": { 904 | "userinfo.token.claim": "true", 905 | "user.attribute": "updatedAt", 906 | "id.token.claim": "true", 907 | "access.token.claim": "true", 908 | "claim.name": "updated_at", 909 | "jsonType.label": "long" 910 | } 911 | } 912 | ] 913 | } 914 | ], 915 | "defaultDefaultClientScopes": [ 916 | "role_list", 917 | "profile", 918 | "email", 919 | "roles", 920 | "web-origins", 921 | "acr" 922 | ], 923 | "defaultOptionalClientScopes": [ 924 | "offline_access", 925 | "address", 926 | "phone", 927 | "microprofile-jwt" 928 | ], 929 | "browserSecurityHeaders": { 930 | "contentSecurityPolicyReportOnly": "", 931 | "xContentTypeOptions": "nosniff", 932 | "referrerPolicy": "no-referrer", 933 | "xRobotsTag": "none", 934 | "xFrameOptions": "SAMEORIGIN", 935 | "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", 936 | "xXSSProtection": "1; mode=block", 937 | "strictTransportSecurity": "max-age=31536000; includeSubDomains" 938 | }, 939 | "smtpServer": {}, 940 | "eventsEnabled": false, 941 | "eventsListeners": [ 942 | "jboss-logging" 943 | ], 944 | "enabledEventTypes": [], 945 | "adminEventsEnabled": false, 946 | "adminEventsDetailsEnabled": false, 947 | "identityProviders": [], 948 | "identityProviderMappers": [], 949 | "components": { 950 | "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ 951 | { 952 | "id": "48899fd6-2798-4155-87dd-7ac2c6c2f72f", 953 | "name": "Allowed Client Scopes", 954 | "providerId": "allowed-client-templates", 955 | "subType": "authenticated", 956 | "subComponents": {}, 957 | "config": { 958 | "allow-default-scopes": [ 959 | "true" 960 | ] 961 | } 962 | }, 963 | { 964 | "id": "77dfaff3-bc24-489a-8485-c9528dca999c", 965 | "name": "Allowed Protocol Mapper Types", 966 | "providerId": "allowed-protocol-mappers", 967 | "subType": "anonymous", 968 | "subComponents": {}, 969 | "config": { 970 | "allowed-protocol-mapper-types": [ 971 | "saml-role-list-mapper", 972 | "saml-user-property-mapper", 973 | "oidc-full-name-mapper", 974 | "oidc-address-mapper", 975 | "oidc-usermodel-attribute-mapper", 976 | "oidc-sha256-pairwise-sub-mapper", 977 | "oidc-usermodel-property-mapper", 978 | "saml-user-attribute-mapper" 979 | ] 980 | } 981 | }, 982 | { 983 | "id": "ba87f7eb-9c2f-4f75-8f8c-db11fdb74eaa", 984 | "name": "Full Scope Disabled", 985 | "providerId": "scope", 986 | "subType": "anonymous", 987 | "subComponents": {}, 988 | "config": {} 989 | }, 990 | { 991 | "id": "981d1165-bd59-421e-9589-b6dc81d6f87b", 992 | "name": "Consent Required", 993 | "providerId": "consent-required", 994 | "subType": "anonymous", 995 | "subComponents": {}, 996 | "config": {} 997 | }, 998 | { 999 | "id": "1d3f4d16-ad48-4de9-8957-574575a487b6", 1000 | "name": "Max Clients Limit", 1001 | "providerId": "max-clients", 1002 | "subType": "anonymous", 1003 | "subComponents": {}, 1004 | "config": { 1005 | "max-clients": [ 1006 | "200" 1007 | ] 1008 | } 1009 | }, 1010 | { 1011 | "id": "ffb888eb-e9d0-4549-9c15-a9cfe5ee8ce3", 1012 | "name": "Allowed Client Scopes", 1013 | "providerId": "allowed-client-templates", 1014 | "subType": "anonymous", 1015 | "subComponents": {}, 1016 | "config": { 1017 | "allow-default-scopes": [ 1018 | "true" 1019 | ] 1020 | } 1021 | }, 1022 | { 1023 | "id": "47984ad9-3db9-4d8d-8c99-e8569bfb113c", 1024 | "name": "Allowed Protocol Mapper Types", 1025 | "providerId": "allowed-protocol-mappers", 1026 | "subType": "authenticated", 1027 | "subComponents": {}, 1028 | "config": { 1029 | "allowed-protocol-mapper-types": [ 1030 | "saml-user-attribute-mapper", 1031 | "oidc-usermodel-property-mapper", 1032 | "saml-role-list-mapper", 1033 | "oidc-full-name-mapper", 1034 | "oidc-sha256-pairwise-sub-mapper", 1035 | "oidc-usermodel-attribute-mapper", 1036 | "oidc-address-mapper", 1037 | "saml-user-property-mapper" 1038 | ] 1039 | } 1040 | }, 1041 | { 1042 | "id": "8e240cdb-a355-4fa5-91c4-7ec5cd515d69", 1043 | "name": "Trusted Hosts", 1044 | "providerId": "trusted-hosts", 1045 | "subType": "anonymous", 1046 | "subComponents": {}, 1047 | "config": { 1048 | "host-sending-registration-request-must-match": [ 1049 | "true" 1050 | ], 1051 | "client-uris-must-match": [ 1052 | "true" 1053 | ] 1054 | } 1055 | } 1056 | ], 1057 | "org.keycloak.keys.KeyProvider": [ 1058 | { 1059 | "id": "0d10601a-b4d4-4583-bd68-225207abc244", 1060 | "name": "hmac-generated", 1061 | "providerId": "hmac-generated", 1062 | "subComponents": {}, 1063 | "config": { 1064 | "priority": [ 1065 | "100" 1066 | ], 1067 | "algorithm": [ 1068 | "HS256" 1069 | ] 1070 | } 1071 | }, 1072 | { 1073 | "id": "3cb34b93-0d5f-48c3-89f6-23c6db611073", 1074 | "name": "aes-generated", 1075 | "providerId": "aes-generated", 1076 | "subComponents": {}, 1077 | "config": { 1078 | "priority": [ 1079 | "100" 1080 | ] 1081 | } 1082 | }, 1083 | { 1084 | "id": "d708af13-78e3-4ca8-960f-dee9b13ad8f1", 1085 | "name": "rsa-enc-generated", 1086 | "providerId": "rsa-enc-generated", 1087 | "subComponents": {}, 1088 | "config": { 1089 | "priority": [ 1090 | "100" 1091 | ], 1092 | "algorithm": [ 1093 | "RSA-OAEP" 1094 | ] 1095 | } 1096 | }, 1097 | { 1098 | "id": "96aeefca-ccb4-40f5-bdae-6c21c41f64c0", 1099 | "name": "rsa-generated", 1100 | "providerId": "rsa-generated", 1101 | "subComponents": {}, 1102 | "config": { 1103 | "priority": [ 1104 | "100" 1105 | ] 1106 | } 1107 | } 1108 | ] 1109 | }, 1110 | "internationalizationEnabled": false, 1111 | "supportedLocales": [], 1112 | "authenticationFlows": [ 1113 | { 1114 | "id": "88635639-b59c-4f3f-b8d8-49a67faa2a85", 1115 | "alias": "Account verification options", 1116 | "description": "Method with which to verity the existing account", 1117 | "providerId": "basic-flow", 1118 | "topLevel": false, 1119 | "builtIn": true, 1120 | "authenticationExecutions": [ 1121 | { 1122 | "authenticator": "idp-email-verification", 1123 | "authenticatorFlow": false, 1124 | "requirement": "ALTERNATIVE", 1125 | "priority": 10, 1126 | "autheticatorFlow": false, 1127 | "userSetupAllowed": false 1128 | }, 1129 | { 1130 | "authenticatorFlow": true, 1131 | "requirement": "ALTERNATIVE", 1132 | "priority": 20, 1133 | "autheticatorFlow": true, 1134 | "flowAlias": "Verify Existing Account by Re-authentication", 1135 | "userSetupAllowed": false 1136 | } 1137 | ] 1138 | }, 1139 | { 1140 | "id": "c2c32425-5fdc-418f-bcf2-49c8e4723bb1", 1141 | "alias": "Browser - Conditional OTP", 1142 | "description": "Flow to determine if the OTP is required for the authentication", 1143 | "providerId": "basic-flow", 1144 | "topLevel": false, 1145 | "builtIn": true, 1146 | "authenticationExecutions": [ 1147 | { 1148 | "authenticator": "conditional-user-configured", 1149 | "authenticatorFlow": false, 1150 | "requirement": "REQUIRED", 1151 | "priority": 10, 1152 | "autheticatorFlow": false, 1153 | "userSetupAllowed": false 1154 | }, 1155 | { 1156 | "authenticator": "auth-otp-form", 1157 | "authenticatorFlow": false, 1158 | "requirement": "REQUIRED", 1159 | "priority": 20, 1160 | "autheticatorFlow": false, 1161 | "userSetupAllowed": false 1162 | } 1163 | ] 1164 | }, 1165 | { 1166 | "id": "f67cafc8-d118-4824-9cc5-a704f6e53d32", 1167 | "alias": "Browser with user attribute", 1168 | "description": "browser based authentication", 1169 | "providerId": "basic-flow", 1170 | "topLevel": true, 1171 | "builtIn": false, 1172 | "authenticationExecutions": [ 1173 | { 1174 | "authenticator": "auth-cookie", 1175 | "authenticatorFlow": false, 1176 | "requirement": "ALTERNATIVE", 1177 | "priority": 10, 1178 | "autheticatorFlow": false, 1179 | "userSetupAllowed": false 1180 | }, 1181 | { 1182 | "authenticator": "auth-spnego", 1183 | "authenticatorFlow": false, 1184 | "requirement": "DISABLED", 1185 | "priority": 20, 1186 | "autheticatorFlow": false, 1187 | "userSetupAllowed": false 1188 | }, 1189 | { 1190 | "authenticator": "identity-provider-redirector", 1191 | "authenticatorFlow": false, 1192 | "requirement": "ALTERNATIVE", 1193 | "priority": 25, 1194 | "autheticatorFlow": false, 1195 | "userSetupAllowed": false 1196 | }, 1197 | { 1198 | "authenticatorFlow": true, 1199 | "requirement": "ALTERNATIVE", 1200 | "priority": 30, 1201 | "autheticatorFlow": true, 1202 | "flowAlias": "Browser with user attribute forms", 1203 | "userSetupAllowed": false 1204 | } 1205 | ] 1206 | }, 1207 | { 1208 | "id": "4810d943-bba2-4796-8883-b4347fcd5fe9", 1209 | "alias": "Browser with user attribute Browser - Conditional OTP", 1210 | "description": "Flow to determine if the OTP is required for the authentication", 1211 | "providerId": "basic-flow", 1212 | "topLevel": false, 1213 | "builtIn": false, 1214 | "authenticationExecutions": [ 1215 | { 1216 | "authenticator": "conditional-user-configured", 1217 | "authenticatorFlow": false, 1218 | "requirement": "REQUIRED", 1219 | "priority": 10, 1220 | "autheticatorFlow": false, 1221 | "userSetupAllowed": false 1222 | }, 1223 | { 1224 | "authenticator": "auth-otp-form", 1225 | "authenticatorFlow": false, 1226 | "requirement": "REQUIRED", 1227 | "priority": 20, 1228 | "autheticatorFlow": false, 1229 | "userSetupAllowed": false 1230 | } 1231 | ] 1232 | }, 1233 | { 1234 | "id": "be489eea-ff3b-4970-bd95-e01edfdcf6ca", 1235 | "alias": "Browser with user attribute forms", 1236 | "description": "Username, password, otp and other auth forms.", 1237 | "providerId": "basic-flow", 1238 | "topLevel": false, 1239 | "builtIn": false, 1240 | "authenticationExecutions": [ 1241 | { 1242 | "authenticatorConfig": "username password favorite number form config", 1243 | "authenticator": "auth-username-password-attr-form", 1244 | "authenticatorFlow": false, 1245 | "requirement": "REQUIRED", 1246 | "priority": 10, 1247 | "autheticatorFlow": false, 1248 | "userSetupAllowed": false 1249 | }, 1250 | { 1251 | "authenticatorFlow": true, 1252 | "requirement": "CONDITIONAL", 1253 | "priority": 21, 1254 | "autheticatorFlow": true, 1255 | "flowAlias": "Browser with user attribute Browser - Conditional OTP", 1256 | "userSetupAllowed": false 1257 | } 1258 | ] 1259 | }, 1260 | { 1261 | "id": "f1c208f3-c3fa-4237-be78-774e698b379d", 1262 | "alias": "Direct Grant - Conditional OTP", 1263 | "description": "Flow to determine if the OTP is required for the authentication", 1264 | "providerId": "basic-flow", 1265 | "topLevel": false, 1266 | "builtIn": true, 1267 | "authenticationExecutions": [ 1268 | { 1269 | "authenticator": "conditional-user-configured", 1270 | "authenticatorFlow": false, 1271 | "requirement": "REQUIRED", 1272 | "priority": 10, 1273 | "autheticatorFlow": false, 1274 | "userSetupAllowed": false 1275 | }, 1276 | { 1277 | "authenticator": "direct-grant-validate-otp", 1278 | "authenticatorFlow": false, 1279 | "requirement": "REQUIRED", 1280 | "priority": 20, 1281 | "autheticatorFlow": false, 1282 | "userSetupAllowed": false 1283 | } 1284 | ] 1285 | }, 1286 | { 1287 | "id": "f1e9aeac-46b2-4ed2-b362-5f4a651d522b", 1288 | "alias": "First broker login - Conditional OTP", 1289 | "description": "Flow to determine if the OTP is required for the authentication", 1290 | "providerId": "basic-flow", 1291 | "topLevel": false, 1292 | "builtIn": true, 1293 | "authenticationExecutions": [ 1294 | { 1295 | "authenticator": "conditional-user-configured", 1296 | "authenticatorFlow": false, 1297 | "requirement": "REQUIRED", 1298 | "priority": 10, 1299 | "autheticatorFlow": false, 1300 | "userSetupAllowed": false 1301 | }, 1302 | { 1303 | "authenticator": "auth-otp-form", 1304 | "authenticatorFlow": false, 1305 | "requirement": "REQUIRED", 1306 | "priority": 20, 1307 | "autheticatorFlow": false, 1308 | "userSetupAllowed": false 1309 | } 1310 | ] 1311 | }, 1312 | { 1313 | "id": "891b8796-4a38-4189-b462-d4976b7347b4", 1314 | "alias": "Handle Existing Account", 1315 | "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", 1316 | "providerId": "basic-flow", 1317 | "topLevel": false, 1318 | "builtIn": true, 1319 | "authenticationExecutions": [ 1320 | { 1321 | "authenticator": "idp-confirm-link", 1322 | "authenticatorFlow": false, 1323 | "requirement": "REQUIRED", 1324 | "priority": 10, 1325 | "autheticatorFlow": false, 1326 | "userSetupAllowed": false 1327 | }, 1328 | { 1329 | "authenticatorFlow": true, 1330 | "requirement": "REQUIRED", 1331 | "priority": 20, 1332 | "autheticatorFlow": true, 1333 | "flowAlias": "Account verification options", 1334 | "userSetupAllowed": false 1335 | } 1336 | ] 1337 | }, 1338 | { 1339 | "id": "684a0d69-07f6-4cc2-9e40-38b283fa1093", 1340 | "alias": "Reset - Conditional OTP", 1341 | "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", 1342 | "providerId": "basic-flow", 1343 | "topLevel": false, 1344 | "builtIn": true, 1345 | "authenticationExecutions": [ 1346 | { 1347 | "authenticator": "conditional-user-configured", 1348 | "authenticatorFlow": false, 1349 | "requirement": "REQUIRED", 1350 | "priority": 10, 1351 | "autheticatorFlow": false, 1352 | "userSetupAllowed": false 1353 | }, 1354 | { 1355 | "authenticator": "reset-otp", 1356 | "authenticatorFlow": false, 1357 | "requirement": "REQUIRED", 1358 | "priority": 20, 1359 | "autheticatorFlow": false, 1360 | "userSetupAllowed": false 1361 | } 1362 | ] 1363 | }, 1364 | { 1365 | "id": "fd028343-56cb-455c-9325-a363909edf5d", 1366 | "alias": "User creation or linking", 1367 | "description": "Flow for the existing/non-existing user alternatives", 1368 | "providerId": "basic-flow", 1369 | "topLevel": false, 1370 | "builtIn": true, 1371 | "authenticationExecutions": [ 1372 | { 1373 | "authenticatorConfig": "create unique user config", 1374 | "authenticator": "idp-create-user-if-unique", 1375 | "authenticatorFlow": false, 1376 | "requirement": "ALTERNATIVE", 1377 | "priority": 10, 1378 | "autheticatorFlow": false, 1379 | "userSetupAllowed": false 1380 | }, 1381 | { 1382 | "authenticatorFlow": true, 1383 | "requirement": "ALTERNATIVE", 1384 | "priority": 20, 1385 | "autheticatorFlow": true, 1386 | "flowAlias": "Handle Existing Account", 1387 | "userSetupAllowed": false 1388 | } 1389 | ] 1390 | }, 1391 | { 1392 | "id": "100d0271-3c54-4ba1-9a37-b0078a82470c", 1393 | "alias": "Verify Existing Account by Re-authentication", 1394 | "description": "Reauthentication of existing account", 1395 | "providerId": "basic-flow", 1396 | "topLevel": false, 1397 | "builtIn": true, 1398 | "authenticationExecutions": [ 1399 | { 1400 | "authenticator": "idp-username-password-form", 1401 | "authenticatorFlow": false, 1402 | "requirement": "REQUIRED", 1403 | "priority": 10, 1404 | "autheticatorFlow": false, 1405 | "userSetupAllowed": false 1406 | }, 1407 | { 1408 | "authenticatorFlow": true, 1409 | "requirement": "CONDITIONAL", 1410 | "priority": 20, 1411 | "autheticatorFlow": true, 1412 | "flowAlias": "First broker login - Conditional OTP", 1413 | "userSetupAllowed": false 1414 | } 1415 | ] 1416 | }, 1417 | { 1418 | "id": "05e49cca-3eab-48db-b282-414ed61e37f9", 1419 | "alias": "browser", 1420 | "description": "browser based authentication", 1421 | "providerId": "basic-flow", 1422 | "topLevel": true, 1423 | "builtIn": true, 1424 | "authenticationExecutions": [ 1425 | { 1426 | "authenticator": "auth-cookie", 1427 | "authenticatorFlow": false, 1428 | "requirement": "ALTERNATIVE", 1429 | "priority": 10, 1430 | "autheticatorFlow": false, 1431 | "userSetupAllowed": false 1432 | }, 1433 | { 1434 | "authenticator": "auth-spnego", 1435 | "authenticatorFlow": false, 1436 | "requirement": "DISABLED", 1437 | "priority": 20, 1438 | "autheticatorFlow": false, 1439 | "userSetupAllowed": false 1440 | }, 1441 | { 1442 | "authenticator": "identity-provider-redirector", 1443 | "authenticatorFlow": false, 1444 | "requirement": "ALTERNATIVE", 1445 | "priority": 25, 1446 | "autheticatorFlow": false, 1447 | "userSetupAllowed": false 1448 | }, 1449 | { 1450 | "authenticatorFlow": true, 1451 | "requirement": "ALTERNATIVE", 1452 | "priority": 30, 1453 | "autheticatorFlow": true, 1454 | "flowAlias": "forms", 1455 | "userSetupAllowed": false 1456 | } 1457 | ] 1458 | }, 1459 | { 1460 | "id": "999d9553-e82b-4c06-918c-e82b4544e829", 1461 | "alias": "clients", 1462 | "description": "Base authentication for clients", 1463 | "providerId": "client-flow", 1464 | "topLevel": true, 1465 | "builtIn": true, 1466 | "authenticationExecutions": [ 1467 | { 1468 | "authenticator": "client-secret", 1469 | "authenticatorFlow": false, 1470 | "requirement": "ALTERNATIVE", 1471 | "priority": 10, 1472 | "autheticatorFlow": false, 1473 | "userSetupAllowed": false 1474 | }, 1475 | { 1476 | "authenticator": "client-jwt", 1477 | "authenticatorFlow": false, 1478 | "requirement": "ALTERNATIVE", 1479 | "priority": 20, 1480 | "autheticatorFlow": false, 1481 | "userSetupAllowed": false 1482 | }, 1483 | { 1484 | "authenticator": "client-secret-jwt", 1485 | "authenticatorFlow": false, 1486 | "requirement": "ALTERNATIVE", 1487 | "priority": 30, 1488 | "autheticatorFlow": false, 1489 | "userSetupAllowed": false 1490 | }, 1491 | { 1492 | "authenticator": "client-x509", 1493 | "authenticatorFlow": false, 1494 | "requirement": "ALTERNATIVE", 1495 | "priority": 40, 1496 | "autheticatorFlow": false, 1497 | "userSetupAllowed": false 1498 | } 1499 | ] 1500 | }, 1501 | { 1502 | "id": "7c4c1b16-5c7f-49d8-9603-4c277cdab76a", 1503 | "alias": "direct grant", 1504 | "description": "OpenID Connect Resource Owner Grant", 1505 | "providerId": "basic-flow", 1506 | "topLevel": true, 1507 | "builtIn": true, 1508 | "authenticationExecutions": [ 1509 | { 1510 | "authenticator": "direct-grant-validate-username", 1511 | "authenticatorFlow": false, 1512 | "requirement": "REQUIRED", 1513 | "priority": 10, 1514 | "autheticatorFlow": false, 1515 | "userSetupAllowed": false 1516 | }, 1517 | { 1518 | "authenticator": "direct-grant-validate-password", 1519 | "authenticatorFlow": false, 1520 | "requirement": "REQUIRED", 1521 | "priority": 20, 1522 | "autheticatorFlow": false, 1523 | "userSetupAllowed": false 1524 | }, 1525 | { 1526 | "authenticatorFlow": true, 1527 | "requirement": "CONDITIONAL", 1528 | "priority": 30, 1529 | "autheticatorFlow": true, 1530 | "flowAlias": "Direct Grant - Conditional OTP", 1531 | "userSetupAllowed": false 1532 | } 1533 | ] 1534 | }, 1535 | { 1536 | "id": "b8db3d11-2123-4a26-add1-bc9ea269998d", 1537 | "alias": "docker auth", 1538 | "description": "Used by Docker clients to authenticate against the IDP", 1539 | "providerId": "basic-flow", 1540 | "topLevel": true, 1541 | "builtIn": true, 1542 | "authenticationExecutions": [ 1543 | { 1544 | "authenticator": "docker-http-basic-authenticator", 1545 | "authenticatorFlow": false, 1546 | "requirement": "REQUIRED", 1547 | "priority": 10, 1548 | "autheticatorFlow": false, 1549 | "userSetupAllowed": false 1550 | } 1551 | ] 1552 | }, 1553 | { 1554 | "id": "5d1329c6-23d2-4e14-83fb-bf5c98046a7a", 1555 | "alias": "first broker login", 1556 | "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", 1557 | "providerId": "basic-flow", 1558 | "topLevel": true, 1559 | "builtIn": true, 1560 | "authenticationExecutions": [ 1561 | { 1562 | "authenticatorConfig": "review profile config", 1563 | "authenticator": "idp-review-profile", 1564 | "authenticatorFlow": false, 1565 | "requirement": "REQUIRED", 1566 | "priority": 10, 1567 | "autheticatorFlow": false, 1568 | "userSetupAllowed": false 1569 | }, 1570 | { 1571 | "authenticatorFlow": true, 1572 | "requirement": "REQUIRED", 1573 | "priority": 20, 1574 | "autheticatorFlow": true, 1575 | "flowAlias": "User creation or linking", 1576 | "userSetupAllowed": false 1577 | } 1578 | ] 1579 | }, 1580 | { 1581 | "id": "f28eceb4-1666-439c-b3f7-a4b27d66156d", 1582 | "alias": "forms", 1583 | "description": "Username, password, otp and other auth forms.", 1584 | "providerId": "basic-flow", 1585 | "topLevel": false, 1586 | "builtIn": true, 1587 | "authenticationExecutions": [ 1588 | { 1589 | "authenticator": "auth-username-password-form", 1590 | "authenticatorFlow": false, 1591 | "requirement": "REQUIRED", 1592 | "priority": 10, 1593 | "autheticatorFlow": false, 1594 | "userSetupAllowed": false 1595 | }, 1596 | { 1597 | "authenticatorFlow": true, 1598 | "requirement": "CONDITIONAL", 1599 | "priority": 20, 1600 | "autheticatorFlow": true, 1601 | "flowAlias": "Browser - Conditional OTP", 1602 | "userSetupAllowed": false 1603 | } 1604 | ] 1605 | }, 1606 | { 1607 | "id": "a1983ca1-0bd5-455f-9213-d4adfdc7be36", 1608 | "alias": "registration", 1609 | "description": "registration flow", 1610 | "providerId": "basic-flow", 1611 | "topLevel": true, 1612 | "builtIn": true, 1613 | "authenticationExecutions": [ 1614 | { 1615 | "authenticator": "registration-page-form", 1616 | "authenticatorFlow": true, 1617 | "requirement": "REQUIRED", 1618 | "priority": 10, 1619 | "autheticatorFlow": true, 1620 | "flowAlias": "registration form", 1621 | "userSetupAllowed": false 1622 | } 1623 | ] 1624 | }, 1625 | { 1626 | "id": "ed976ba1-2643-4a0d-9459-02a4ea707535", 1627 | "alias": "registration form", 1628 | "description": "registration form", 1629 | "providerId": "form-flow", 1630 | "topLevel": false, 1631 | "builtIn": true, 1632 | "authenticationExecutions": [ 1633 | { 1634 | "authenticator": "registration-user-creation", 1635 | "authenticatorFlow": false, 1636 | "requirement": "REQUIRED", 1637 | "priority": 20, 1638 | "autheticatorFlow": false, 1639 | "userSetupAllowed": false 1640 | }, 1641 | { 1642 | "authenticator": "registration-profile-action", 1643 | "authenticatorFlow": false, 1644 | "requirement": "REQUIRED", 1645 | "priority": 40, 1646 | "autheticatorFlow": false, 1647 | "userSetupAllowed": false 1648 | }, 1649 | { 1650 | "authenticator": "registration-password-action", 1651 | "authenticatorFlow": false, 1652 | "requirement": "REQUIRED", 1653 | "priority": 50, 1654 | "autheticatorFlow": false, 1655 | "userSetupAllowed": false 1656 | }, 1657 | { 1658 | "authenticator": "registration-recaptcha-action", 1659 | "authenticatorFlow": false, 1660 | "requirement": "DISABLED", 1661 | "priority": 60, 1662 | "autheticatorFlow": false, 1663 | "userSetupAllowed": false 1664 | } 1665 | ] 1666 | }, 1667 | { 1668 | "id": "1c016b4a-acdf-41a8-a02b-124e5bb83768", 1669 | "alias": "reset credentials", 1670 | "description": "Reset credentials for a user if they forgot their password or something", 1671 | "providerId": "basic-flow", 1672 | "topLevel": true, 1673 | "builtIn": true, 1674 | "authenticationExecutions": [ 1675 | { 1676 | "authenticator": "reset-credentials-choose-user", 1677 | "authenticatorFlow": false, 1678 | "requirement": "REQUIRED", 1679 | "priority": 10, 1680 | "autheticatorFlow": false, 1681 | "userSetupAllowed": false 1682 | }, 1683 | { 1684 | "authenticator": "reset-credential-email", 1685 | "authenticatorFlow": false, 1686 | "requirement": "REQUIRED", 1687 | "priority": 20, 1688 | "autheticatorFlow": false, 1689 | "userSetupAllowed": false 1690 | }, 1691 | { 1692 | "authenticator": "reset-password", 1693 | "authenticatorFlow": false, 1694 | "requirement": "REQUIRED", 1695 | "priority": 30, 1696 | "autheticatorFlow": false, 1697 | "userSetupAllowed": false 1698 | }, 1699 | { 1700 | "authenticatorFlow": true, 1701 | "requirement": "CONDITIONAL", 1702 | "priority": 40, 1703 | "autheticatorFlow": true, 1704 | "flowAlias": "Reset - Conditional OTP", 1705 | "userSetupAllowed": false 1706 | } 1707 | ] 1708 | }, 1709 | { 1710 | "id": "eb3a81e5-2e3d-490c-8898-9cca7d387cd4", 1711 | "alias": "saml ecp", 1712 | "description": "SAML ECP Profile Authentication Flow", 1713 | "providerId": "basic-flow", 1714 | "topLevel": true, 1715 | "builtIn": true, 1716 | "authenticationExecutions": [ 1717 | { 1718 | "authenticator": "http-basic-authenticator", 1719 | "authenticatorFlow": false, 1720 | "requirement": "REQUIRED", 1721 | "priority": 10, 1722 | "autheticatorFlow": false, 1723 | "userSetupAllowed": false 1724 | } 1725 | ] 1726 | } 1727 | ], 1728 | "authenticatorConfig": [ 1729 | { 1730 | "id": "6d7f7e83-1ec2-4ddb-a8c3-55c5f27a96d5", 1731 | "alias": "create unique user config", 1732 | "config": { 1733 | "require.password.update.after.registration": "false" 1734 | } 1735 | }, 1736 | { 1737 | "id": "fb542612-8bc7-43dd-8cc0-b74493100e67", 1738 | "alias": "review profile config", 1739 | "config": { 1740 | "update.profile.on.first.login": "missing" 1741 | } 1742 | }, 1743 | { 1744 | "id": "7eb11994-09b3-4df2-807b-e1b1b5ce9cea", 1745 | "alias": "username password favorite number form config", 1746 | "config": { 1747 | "user_attribute": "favorite_number", 1748 | "generate_label": "true", 1749 | "user_attribute_label": "" 1750 | } 1751 | } 1752 | ], 1753 | "requiredActions": [ 1754 | { 1755 | "alias": "CONFIGURE_TOTP", 1756 | "name": "Configure OTP", 1757 | "providerId": "CONFIGURE_TOTP", 1758 | "enabled": true, 1759 | "defaultAction": false, 1760 | "priority": 10, 1761 | "config": {} 1762 | }, 1763 | { 1764 | "alias": "TERMS_AND_CONDITIONS", 1765 | "name": "Terms and Conditions", 1766 | "providerId": "TERMS_AND_CONDITIONS", 1767 | "enabled": false, 1768 | "defaultAction": false, 1769 | "priority": 20, 1770 | "config": {} 1771 | }, 1772 | { 1773 | "alias": "UPDATE_PASSWORD", 1774 | "name": "Update Password", 1775 | "providerId": "UPDATE_PASSWORD", 1776 | "enabled": true, 1777 | "defaultAction": false, 1778 | "priority": 30, 1779 | "config": {} 1780 | }, 1781 | { 1782 | "alias": "UPDATE_PROFILE", 1783 | "name": "Update Profile", 1784 | "providerId": "UPDATE_PROFILE", 1785 | "enabled": true, 1786 | "defaultAction": false, 1787 | "priority": 40, 1788 | "config": {} 1789 | }, 1790 | { 1791 | "alias": "VERIFY_EMAIL", 1792 | "name": "Verify Email", 1793 | "providerId": "VERIFY_EMAIL", 1794 | "enabled": true, 1795 | "defaultAction": false, 1796 | "priority": 50, 1797 | "config": {} 1798 | }, 1799 | { 1800 | "alias": "delete_account", 1801 | "name": "Delete Account", 1802 | "providerId": "delete_account", 1803 | "enabled": false, 1804 | "defaultAction": false, 1805 | "priority": 60, 1806 | "config": {} 1807 | }, 1808 | { 1809 | "alias": "webauthn-register", 1810 | "name": "Webauthn Register", 1811 | "providerId": "webauthn-register", 1812 | "enabled": true, 1813 | "defaultAction": false, 1814 | "priority": 70, 1815 | "config": {} 1816 | }, 1817 | { 1818 | "alias": "webauthn-register-passwordless", 1819 | "name": "Webauthn Register Passwordless", 1820 | "providerId": "webauthn-register-passwordless", 1821 | "enabled": true, 1822 | "defaultAction": false, 1823 | "priority": 80, 1824 | "config": {} 1825 | }, 1826 | { 1827 | "alias": "update_user_locale", 1828 | "name": "Update User Locale", 1829 | "providerId": "update_user_locale", 1830 | "enabled": true, 1831 | "defaultAction": false, 1832 | "priority": 1000, 1833 | "config": {} 1834 | } 1835 | ], 1836 | "browserFlow": "browser", 1837 | "registrationFlow": "registration", 1838 | "directGrantFlow": "direct grant", 1839 | "resetCredentialsFlow": "reset credentials", 1840 | "clientAuthenticationFlow": "clients", 1841 | "dockerAuthenticationFlow": "docker auth", 1842 | "attributes": { 1843 | "cibaBackchannelTokenDeliveryMode": "poll", 1844 | "cibaExpiresIn": "120", 1845 | "cibaAuthRequestedUserHint": "login_hint", 1846 | "oauth2DeviceCodeLifespan": "600", 1847 | "oauth2DevicePollingInterval": "5", 1848 | "parRequestUriLifespan": "60", 1849 | "cibaInterval": "5", 1850 | "realmReusableOtpCode": "false" 1851 | }, 1852 | "keycloakVersion": "22.0.3", 1853 | "userManagedAccessAllowed": false, 1854 | "clientProfiles": { 1855 | "profiles": [] 1856 | }, 1857 | "clientPolicies": { 1858 | "policies": [] 1859 | }, 1860 | "users" : [ { 1861 | "id" : "462c7cb4-e444-4eac-8d68-0686c30eb245", 1862 | "createdTimestamp" : 1621784842260, 1863 | "username" : "test", 1864 | "enabled" : true, 1865 | "totp" : false, 1866 | "emailVerified" : false, 1867 | "attributes" : { 1868 | "favorite_number" : [ "46" ] 1869 | }, 1870 | "credentials" : [ { 1871 | "id" : "a0f53e85-a522-4ab4-85cb-95224dec8d35", 1872 | "type" : "password", 1873 | "createdDate" : 1621784863307, 1874 | "secretData" : "{\"value\":\"MFcHPyUSTiRqMhJum6z9KSIZbwJZAUswqq1zoVjoA6Cse8iylnjw9fOkEO72IWgS+PIj3RW7WB/CUp2deW8Swg==\",\"salt\":\"WztZBrFIbqNmEMGkQer7eQ==\",\"additionalParameters\":{}}", 1875 | "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" 1876 | } ], 1877 | "requiredActions" : [ ], 1878 | "realmRoles" : [ "default-roles-dev-realm" ], 1879 | "notBefore" : 0, 1880 | "groups" : [ ] 1881 | } ] 1882 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | keycloak: 5 | container_name: keycloak 6 | image: quay.io/keycloak/keycloak:24.0.1 7 | entrypoint: [ "/opt/keycloak/bin/kc.sh", "--verbose", "start-dev", "--import-realm" ] 8 | environment: 9 | DEBUG: 'true' 10 | DEBUG_PORT: '*:8787' 11 | KC_PROXY: edge 12 | KC_HTTP_PORT: 8080 13 | KEYCLOAK_ADMIN: admin 14 | KEYCLOAK_ADMIN_PASSWORD: admin 15 | volumes: 16 | - type: bind 17 | source: ./target 18 | target: /opt/keycloak/providers 19 | - type: bind 20 | source: ./dev-realm.json 21 | target: /opt/keycloak/data/import/dev-realm.json 22 | ports: 23 | - '8080:8080' 24 | - '8787:8787' 25 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | Keycloak username password attribute authenticator 8 | 9 | keycloak-username-password-attribute-authenticator 10 | io.github.kilmajster 11 | SNAPSHOT 12 | 13 | jar 14 | 15 | Default Keycloak login form with additional user attribute validation 16 | https://github.com/kilmajster/keycloak-username-password-attribute-authenticator 17 | 2021 18 | 19 | 20 | 21 | MIT 22 | https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/blob/main/LICENSE 23 | repo 24 | 25 | 26 | 27 | 28 | UTF-8 29 | 17 30 | 17 31 | 32 | 24.0.5 33 | 34 | 3.12.1 35 | 3.1.0 36 | 3.3.0 37 | 3.6.3 38 | 3.3.0 39 | 3.2.5 40 | 41 | 42 | 43 | 44 | org.keycloak 45 | keycloak-services 46 | ${keycloak.version} 47 | provided 48 | 49 | 50 | org.keycloak 51 | keycloak-server-spi 52 | ${keycloak.version} 53 | provided 54 | 55 | 56 | org.keycloak 57 | keycloak-server-spi-private 58 | ${keycloak.version} 59 | provided 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/main/java/io/github/kilmajster/keycloak/UsernamePasswordAttributeForm.java: -------------------------------------------------------------------------------- 1 | package io.github.kilmajster.keycloak; 2 | 3 | import jakarta.ws.rs.core.MultivaluedMap; 4 | import jakarta.ws.rs.core.Response; 5 | import org.keycloak.authentication.AuthenticationFlowContext; 6 | import org.keycloak.authentication.AuthenticationFlowError; 7 | import org.keycloak.authentication.Authenticator; 8 | import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm; 9 | import org.keycloak.events.Details; 10 | import org.keycloak.events.Errors; 11 | import org.keycloak.forms.login.LoginFormsProvider; 12 | import org.keycloak.models.UserModel; 13 | import org.keycloak.models.utils.FormMessage; 14 | import org.keycloak.services.ServicesLogger; 15 | 16 | import static io.github.kilmajster.keycloak.UsernamePasswordAttributeFormConfiguration.*; 17 | import static org.keycloak.services.validation.Validation.FIELD_PASSWORD; 18 | 19 | public class UsernamePasswordAttributeForm extends UsernamePasswordForm implements Authenticator { 20 | 21 | protected static ServicesLogger log = ServicesLogger.LOGGER; 22 | 23 | @Override 24 | protected Response challenge(AuthenticationFlowContext context, String error, String field) { 25 | setUserAttributeFormLabel(context); 26 | setUserAttributeFormErrorMessage(context); 27 | 28 | return super.challenge(context, null, null); 29 | } 30 | 31 | @Override 32 | protected Response challenge(AuthenticationFlowContext context, MultivaluedMap formData) { 33 | setUserAttributeFormLabel(context); 34 | 35 | return super.challenge(context, formData); 36 | } 37 | 38 | @Override 39 | protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap formData) { 40 | return super.validateForm(context, formData) 41 | && isUserAttributeValid(context, formData.getFirst(USER_ATTRIBUTE)); 42 | } 43 | 44 | private void setUserAttributeFormLabel(AuthenticationFlowContext context) { 45 | String userAttributeLabel = configPropertyOf(context, USER_ATTRIBUTE_LABEL); 46 | 47 | // label 48 | if (userAttributeLabel != null) { 49 | context.form().setAttribute(USER_ATTRIBUTE_LABEL, userAttributeLabel); 50 | } else { 51 | String userAttributeName = configPropertyOf(context, USER_ATTRIBUTE); 52 | if (userAttributeName != null && !userAttributeName.isEmpty()) { 53 | context.form().setAttribute( 54 | USER_ATTRIBUTE_LABEL, 55 | isGenerateLabelEnabled(context) 56 | ? generateLabel(userAttributeName, true) 57 | : userAttributeName 58 | ); 59 | } else { 60 | log.warn("Configuration of keycloak-user-attribute-authenticator is incomplete! " + 61 | "At least user_attribute property needs to be set!"); 62 | } 63 | } 64 | } 65 | 66 | private void setUserAttributeFormErrorMessage(AuthenticationFlowContext context) { 67 | String userAttributeName = configPropertyOf(context, USER_ATTRIBUTE); 68 | String userAttributeErrorMessage = configPropertyOf(context, USER_ATTRIBUTE_ERROR_MESSAGE); 69 | 70 | if (userAttributeErrorMessage != null) { 71 | context.form().addError( 72 | new FormMessage(FIELD_PASSWORD, userAttributeErrorMessage, userAttributeName) 73 | ); 74 | } else { 75 | if (userAttributeName != null && !userAttributeName.isEmpty()) { 76 | context.form().addError( 77 | new FormMessage( 78 | FIELD_PASSWORD, 79 | "invalidUsernamePasswordOrAttributeMessage", 80 | isGenerateLabelEnabled(context) 81 | ? generateLabel(userAttributeName, false) 82 | : userAttributeName 83 | ) 84 | ); 85 | } else { 86 | log.warn("Configuration of keycloak-user-attribute-authenticator is incomplete! " + 87 | "At least user_attribute property needs to be set!"); 88 | } 89 | } 90 | } 91 | 92 | private boolean isUserAttributeValid(AuthenticationFlowContext context, String providedAttribute) { 93 | String attributeName = context.getAuthenticatorConfig().getConfig().get(USER_ATTRIBUTE); 94 | UserModel user = context.getUser(); 95 | boolean attributeValid = user != null 96 | && user.getAttributeStream(attributeName).anyMatch(attr -> attr.equals(providedAttribute)); 97 | 98 | return attributeValid || badAttributeHandler(context, user); 99 | } 100 | 101 | private boolean badAttributeHandler(AuthenticationFlowContext context, UserModel user) { 102 | context.getEvent().user(user); 103 | context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); 104 | context.getEvent().detail(Details.REASON, "Invalid user attribute was provided"); 105 | 106 | if (isUserAlreadySetBeforeUsernamePasswordAuth(context)) { 107 | LoginFormsProvider form = context.form(); 108 | form.setAttribute(LoginFormsProvider.USERNAME_HIDDEN, true); 109 | form.setAttribute(LoginFormsProvider.REGISTRATION_DISABLED, true); 110 | } 111 | 112 | Response challengeResponse = challenge(context, getDefaultChallengeMessage(context), FIELD_PASSWORD); 113 | context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse); 114 | context.clearUser(); 115 | 116 | return false; 117 | } 118 | 119 | private String generateLabel(final String attributeName, boolean capitalize) { 120 | final String lowercaseWithSpaces = attributeName 121 | .toLowerCase() 122 | .replace(".", " ") 123 | .replace("_", " ") 124 | .replace("-", " "); 125 | 126 | return capitalize ? capitalize(lowercaseWithSpaces) : lowercaseWithSpaces; 127 | } 128 | 129 | private String capitalize(final String toCapitalize) { 130 | return toCapitalize.substring(0, 1).toUpperCase() + toCapitalize.substring(1).toLowerCase(); 131 | } 132 | } -------------------------------------------------------------------------------- /src/main/java/io/github/kilmajster/keycloak/UsernamePasswordAttributeFormConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.github.kilmajster.keycloak; 2 | 3 | import org.keycloak.authentication.AuthenticationFlowContext; 4 | import org.keycloak.provider.ProviderConfigProperty; 5 | import org.keycloak.provider.ProviderConfigurationBuilder; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public interface UsernamePasswordAttributeFormConfiguration { 11 | 12 | String USER_ATTRIBUTE = "user_attribute"; 13 | String GENERATE_LABEL = "generate_label"; 14 | String USER_ATTRIBUTE_LABEL = "user_attribute_label"; 15 | String USER_ATTRIBUTE_ERROR_MESSAGE = "user_attribute_error_message"; 16 | 17 | List PROPS = ProviderConfigurationBuilder.create() 18 | 19 | .property() 20 | .name(USER_ATTRIBUTE) 21 | .type(ProviderConfigProperty.STRING_TYPE) 22 | .label("User attribute") 23 | .helpText("Attribute used to validate login form.") 24 | .add() 25 | 26 | .property() 27 | .name(GENERATE_LABEL) 28 | .type(ProviderConfigProperty.BOOLEAN_TYPE) 29 | .label("Generate label") 30 | .defaultValue("true") // only string value is accepted 31 | .helpText("If enabled, label for login form will be generated based on attribute name, so attribute with name:" + 32 | " \"favorite_number\" will be labeled as \"Favorite number\", \"REALLY_custom.user-Attribute\" will be translated " + 33 | "to \"Really custom user attribute\", etc. By default, set to true. If User attribute form label " + 34 | "is configured, label is taken form configuration and generation is skipped.") 35 | .add() 36 | 37 | .property() 38 | .name(USER_ATTRIBUTE_LABEL) 39 | .type(ProviderConfigProperty.STRING_TYPE) 40 | .label("User attribute form label") 41 | .helpText("Message which will be displayed as user attribute input label. If value is a valid message key, then proper translation will be used.") 42 | .add() 43 | 44 | .property() 45 | .name(USER_ATTRIBUTE_ERROR_MESSAGE) 46 | .type(ProviderConfigProperty.STRING_TYPE) 47 | .label("Invalid user attribute error message") 48 | .helpText("Message which will be displayed for invalid user attribute error message. If value is a valid message key, then proper translation will be used.") 49 | .add() 50 | 51 | .build(); 52 | 53 | static String configPropertyOf(final AuthenticationFlowContext context, final String configPropertyName) { 54 | return Optional.ofNullable(System.getenv(configPropertyName.toUpperCase())) 55 | .orElse(context.getAuthenticatorConfig().getConfig().get(configPropertyName)); 56 | } 57 | 58 | static boolean isGenerateLabelEnabled(final AuthenticationFlowContext context) { 59 | return Boolean.parseBoolean(configPropertyOf(context, GENERATE_LABEL)); 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/java/io/github/kilmajster/keycloak/UsernamePasswordAttributeFormFactory.java: -------------------------------------------------------------------------------- 1 | package io.github.kilmajster.keycloak; 2 | 3 | import org.keycloak.Config; 4 | import org.keycloak.authentication.Authenticator; 5 | import org.keycloak.authentication.AuthenticatorFactory; 6 | import org.keycloak.models.AuthenticationExecutionModel; 7 | import org.keycloak.models.KeycloakSession; 8 | import org.keycloak.models.KeycloakSessionFactory; 9 | import org.keycloak.models.credential.PasswordCredentialModel; 10 | import org.keycloak.provider.ProviderConfigProperty; 11 | 12 | import java.util.List; 13 | 14 | public class UsernamePasswordAttributeFormFactory implements AuthenticatorFactory { 15 | 16 | public static final String PROVIDER_ID = "auth-username-password-attr-form"; 17 | public static final UsernamePasswordAttributeForm SINGLETON = new UsernamePasswordAttributeForm(); 18 | 19 | @Override 20 | public Authenticator create(KeycloakSession session) { 21 | return SINGLETON; 22 | } 23 | 24 | @Override 25 | public List getConfigProperties() { 26 | return UsernamePasswordAttributeFormConfiguration.PROPS; 27 | } 28 | 29 | @Override 30 | public void init(Config.Scope config) { 31 | 32 | } 33 | 34 | @Override 35 | public void postInit(KeycloakSessionFactory factory) { 36 | 37 | } 38 | 39 | @Override 40 | public void close() { 41 | 42 | } 43 | 44 | @Override 45 | public String getId() { 46 | return PROVIDER_ID; 47 | } 48 | 49 | @Override 50 | public String getReferenceCategory() { 51 | return PasswordCredentialModel.TYPE; 52 | } 53 | 54 | @Override 55 | public boolean isConfigurable() { 56 | return true; 57 | } 58 | 59 | public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { 60 | AuthenticationExecutionModel.Requirement.REQUIRED, 61 | AuthenticationExecutionModel.Requirement.DISABLED, 62 | }; 63 | 64 | @Override 65 | public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { 66 | return REQUIREMENT_CHOICES; 67 | } 68 | 69 | @Override 70 | public String getDisplayType() { 71 | return "Username Password Attribute Form"; 72 | } 73 | 74 | @Override 75 | public String getHelpText() { 76 | return "Validates a username, password and selected user attribute from login form."; 77 | } 78 | 79 | @Override 80 | public boolean isUserSetupAllowed() { 81 | return false; 82 | } 83 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/keycloak-themes.json: -------------------------------------------------------------------------------- 1 | { 2 | "themes": [{ 3 | "name": "base-with-attribute", 4 | "types": ["login"] 5 | }] 6 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory: -------------------------------------------------------------------------------- 1 | io.github.kilmajster.keycloak.UsernamePasswordAttributeFormFactory -------------------------------------------------------------------------------- /src/main/resources/theme/base-with-attribute/login/login.ftl: -------------------------------------------------------------------------------- 1 | <#import "template.ftl" as layout> 2 | <@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section> 3 | <#if section = "header"> 4 | ${msg("loginAccountTitle")} 5 | <#elseif section = "form"> 6 |
7 |
8 | <#if realm.password> 9 |
10 | <#if !usernameHidden??> 11 |
12 | 13 | 14 | 17 | 18 | <#if messagesPerField.existsError('username','password')> 19 | 20 | ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} 21 | 22 | 23 | 24 |
25 | 26 | 27 |
28 | 29 | 30 |
31 | 34 | 40 |
41 | 42 | <#if usernameHidden?? && messagesPerField.existsError('username','password')> 43 | 44 | ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} 45 | 46 | 47 | 48 |
49 | 50 | 51 |
52 | 53 |
54 | 57 | 63 |
64 | <#if usernameHidden?? && messagesPerField.existsError('username','password', 'user_attribute')> 65 | 66 | ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} 67 | 68 | 69 |
70 | 71 | 72 |
73 |
74 | <#if realm.rememberMe && !usernameHidden??> 75 |
76 | 83 |
84 | 85 |
86 |
87 | <#if realm.resetPasswordAllowed> 88 | ${msg("doForgotPassword")} 89 | 90 |
91 | 92 |
93 | 94 |
95 | value="${auth.selectedCredential}"/> 96 | 97 |
98 |
99 | 100 |
101 |
102 | 103 | <#elseif section = "info" > 104 | <#if realm.password && realm.registrationAllowed && !registrationDisabled??> 105 |
106 |
107 | ${msg("noAccount")} ${msg("doRegister")} 109 |
110 |
111 | 112 | <#elseif section = "socialProviders" > 113 | <#if realm.password && social.providers??> 114 |
115 |
116 |

${msg("identity-provider-login-label")}

117 | 118 | 133 |
134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/main/resources/theme/base-with-attribute/login/messages/messages_en.properties: -------------------------------------------------------------------------------- 1 | defaultUserAttributeLabel=User attribute 2 | showUserAttribute=Show attribute 3 | hideUserAttribute=Hide attribute 4 | invalidUsernamePasswordOrAttributeMessage=Invalid username, password or {0}. -------------------------------------------------------------------------------- /src/main/resources/theme/base-with-attribute/login/theme.properties: -------------------------------------------------------------------------------- 1 | parent=keycloak 2 | import=common/keycloak --------------------------------------------------------------------------------