├── .gitignore
├── .idea
└── vcs.xml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build.gradle
├── dist
├── .gitkeep
├── keycloak-configurable-token-0.1.jar
├── keycloak-configurable-token-0.2.jar
├── keycloak-configurable-token-0.3.jar
├── keycloak-configurable-token-0.4.jar
├── keycloak-configurable-token-0.5.jar
├── keycloak-configurable-token-1.0.0.jar
├── keycloak-configurable-token-1.1.0.jar
├── keycloak-configurable-token-1.10.0.jar
├── keycloak-configurable-token-1.2.0.jar
├── keycloak-configurable-token-1.3.0.jar
├── keycloak-configurable-token-1.5.0.jar
├── keycloak-configurable-token-1.5.1.jar
├── keycloak-configurable-token-1.7.0.jar
├── keycloak-configurable-token-1.8.0.jar
├── keycloak-configurable-token-1.9.0.jar
├── keycloak-configurable-token-21.0.0.jar
├── keycloak-configurable-token-21.0.1.jar
├── keycloak-configurable-token-22.0.0.jar
├── keycloak-configurable-token-23.0.0.jar
├── keycloak-configurable-token-24.0.0.jar
├── keycloak-configurable-token-25.0.0.jar
├── keycloak-configurable-token-25.0.1.jar
└── keycloak-configurable-token-26.0.0.jar
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
└── main
├── java
└── be
│ └── looorent
│ ├── ConfigurableTokenResource.java
│ ├── ConfigurableTokenResourceProvider.java
│ ├── ConfigurableTokenResourceProviderFactory.java
│ ├── ConfigurationTokenResourceConfiguration.java
│ └── TokenConfiguration.java
└── resources
└── META-INF
├── jboss-deployment-structure.xml
└── services
└── org.keycloak.services.resource.RealmResourceProviderFactory
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Java
2 | *.class
3 | .gradle
4 | build
5 | out
6 |
7 | ## IntelliJ
8 | .idea
9 | *.iml
10 | *.ipr
11 | *.iws
12 |
13 |
14 | .DS_Store
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 26.0.0 [2024-10-09]
4 |
5 | * Compatibility with Keycloak 26
6 |
7 | ## 25.0.1 [2024-09-20]
8 |
9 | * Bugfix: Since Keycloak 24, some properties of the Session's client (like USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED) are required to create an access token. Therefore the destination session must be associated to a client.
10 |
11 | ## 25.0.0 [2024-06-19]
12 |
13 | * Compatibility with Keycloak 25
14 |
15 | ## 24.0.0 [2024-03-11]
16 |
17 | * Move to Keycloak 24
18 | * Make use of the Cors SPI
19 |
20 | ## 23.0.3 [2023-12-22]
21 |
22 | * Compatibility with Keycloak 23.0.3
23 |
24 | ## 23.0.0 [2023-11-24]
25 |
26 | * Compatibility with Keycloak 23.0.0
27 |
28 | ## 22.0.0 [2023-07-11]
29 |
30 | * Compatibility with Keycloak 22.0.0
31 |
32 | ## 21.0.1 [2023-03-21]
33 |
34 | * Make CORS Allowed origins configurable
35 | * Fix `21.0.0` startup issues
36 |
37 | ## 21.0.0 [2023-03-21]
38 |
39 | * Compatibility with Keycloak 21.0.0
40 |
41 | ## 1.10.0 [2022-11-11]
42 |
43 | * Compatibility with Keycloak 20.0.0
44 |
45 | ## 1.9.0 [2022-07-27]
46 |
47 | * Compatibility with Keycloak 19.0.0
48 |
49 | ## 1.8.0 [2022-04-21]
50 |
51 | * Compatibility with Keycloak 18.0.0
52 |
53 | ## 1.7.0 [2022-01-15]
54 |
55 | * Compatibility with Keycloak 17.0.0
56 |
57 | ## 1.5.1 [2021-08-30]
58 |
59 | * Compatibility with Keycloak 15.0.2
60 | * Update user session's timestamp
61 |
62 | ## 1.5.0 [2021-08-24]
63 |
64 | * Compatibility with Keycloak 15
65 |
66 | ## 1.4.0 [2021-06-18]
67 |
68 | * Compatibility with Keycloak 14
69 |
70 | ## 1.3.0 [2020-12-19]
71 |
72 | * Compatibility with Keycloak 12
73 |
74 | ## 1.2.0 [2020-07-22]
75 |
76 | * Compatibility with Keycloak 11
77 |
78 | ## 1.0.1 [2020-07-01]
79 |
80 | * Introspect access token to give more details when failures occur
81 | * Capability to use long-term tokens are now based on user model instead of the access token content
82 | * Compatibility with Keycloak 10
83 |
84 | ## 1.0.0 [2019-04-18]
85 |
86 | * Compatibility with Keycloak 9
87 |
88 | ## 0.5 [2019-02-19]
89 |
90 | * Tested on Keycloak 4.8.3
91 | * Long-Lived tokens are restricted depending on a realm role
92 |
93 | ## 0.4 [2019-02-13]
94 |
95 | * Support for long-lived tokens
96 |
97 | ## 0.3 [2018-06-19]
98 |
99 | * Support for Keycloak 4.0.0
100 | * Wildfly automatic deployment
101 |
102 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019
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 Configuration Token REST API
2 |
3 | This Custom Keycloak REST API provides an extra endpoint to request a token that can override default configuration.
4 |
5 | It adds an endpoint `POST ${serverDomain}/auth/realms/${realm}/configurable-token`. Its configuration is provided in the request's body.
6 |
7 | This implementation is based on the token exchange principle, defined here: https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/token-exchange/token-exchange.adoc
8 |
9 | ## Keycloak Support
10 |
11 | Pay attention to your Keycloak version!
12 |
13 | * For Keycloak `3.x.x`, use version <= 0.1 of this JAR.
14 | * For Keycloak `4.x.x`, use version > 0.1 of this JAR.
15 | * For Keycloak `9.x.x`, use version >= 1.0.0 of this JAR.
16 | * For Keycloak `10.x.x`, use version >= 1.1.0 of this JAR.
17 | * For Keycloak `11.x.x`, use version >= 1.2.0 of this JAR.
18 | * For Keycloak `12.x.x`, use version >= 1.3.0 of this JAR.
19 | * For Keycloak `14.x.x`, use version >= 1.4.0 of this JAR.
20 | * For Keycloak `15.x.x` < `15.0.2`, use version >= 1.5.0 of this JAR.
21 | * For Keycloak `>= 15.0.2`, use version >= 1.5.1 of this JAR.
22 | * For Keycloak `>= 17.x.x`, use version >= 1.7.0 of this JAR.
23 | * For Keycloak `>= 18.x.x`, use version >= 1.8.0 of this JAR.
24 | * For Keycloak `>= 19.x.x`, use version >= 1.9.0 of this JAR.
25 | * For Keycloak `>= 20.x.x`, use version >= 1.10.0 of this JAR.
26 | * For Keycloak `>= 21.x.x`, use version >= 21.0.0 of this JAR.
27 | * For Keycloak `>= 22.x.x`, use version >= 22.0.0 of this JAR.
28 | * For Keycloak `>= 23.x.x`, use version >= 23.0.0 of this JAR.
29 | * For Keycloak `>= 24.x.x`, use version >= 24.0.0 of this JAR.
30 | * For Keycloak `>= 25.x.x`, use version >= 25.0.0 of this JAR.
31 | * For Keycloak `>= 26.x.x`, use version >= 26.0.0 of this JAR.
32 |
33 | ## Supported features
34 |
35 | * Ask for a short-lived lifespan
36 | * Ask for a long-lived lifespan
37 |
38 | ## Deployment (`>= 0.3`)
39 |
40 | ### Standalone install
41 |
42 | * Download `dist/keycloak-configurable-token-23.0.0.jar` from this repository
43 | * Add it to `$KEYCLOAK_HOME/standalone/deployments/`
44 |
45 | ### Docker install
46 |
47 | If you are using the official Docker image, here is a `Dockerfile` that automate the installation procedure described above:
48 | ```
49 | FROM quay.io/keycloak/keycloak:26.0.0
50 |
51 | COPY keycloak-configurable-token-26.0.0.jar /opt/keycloak/providers/keycloak-configurable-token.jar
52 | ```
53 |
54 | ## Deployment (`< 0.3`)
55 |
56 | Before `0.3`, this library cannot be deployed properly as a module with dependencies without using the CLI.
57 | Therefore, using the CLI is mandatory.
58 |
59 | ### Environment variables
60 |
61 | | Option | Default Value | Type | Required? | Description | Example |
62 | |--------------------------------------|--------------------|----------|-----------|---------------------------------------------------------------------------|----------------------------------------|
63 | | `KEYCLOAK_LONG_LIVED_ROLE_NAME` | `long_lived_token` | String | Optional | The realm role an exchange token must have to request a long-lived-token. | `my-custom-role-for-long-lived-tokens` |
64 | | `KEYCLOAK_LONG_LIVED_CORS_ORIGINS` | `*` | String[] | Optional | The Cors Origins allowed for every HTTP call, separated by a comma | `https://your-website.com` |
65 |
66 |
67 | ### Standalone install
68 |
69 | * Download `dist/keycloak-configurable-token-0.2.jar` from this repository
70 | * Modify `$KEYCLOAK_HOME/standalone/configuration/standalone.xml` and this node in ``
71 | ```xml
72 | module:be.looorent.keycloak-configurable-token
73 | ```
74 | * Run `jboss-cli` to add this module and define their dependencies:
75 | ```bash
76 | $ jboss-cli.sh --command="module add --name=be.looorent.keycloak-configurable-token --resources=keycloak-configurable-token-0.2.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services,org.jboss.logging,javax.ws.rs.api"
77 | ```
78 |
79 | ### Docker install
80 |
81 | If you are using the official Docker image, here is a `Dockerfile` that automate the installation procedure described above:
82 | ```
83 | FROM jboss/keycloak:14.0.0.Final
84 |
85 | COPY keycloak-configurable-token-1.3.0.jar /tmp/keycloak-configurable-token.jar
86 | RUN /opt/jboss/keycloak/bin/jboss-cli.sh --command="module add --name=be.looorent.keycloak-configurable-token --resources=/tmp/keycloak-configurable-token.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-common,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services,org.jboss.logging,javax.ws.rs.api"
87 | RUN sed -i -- 's/classpath:${jboss.home.dir}\/providers\/\*/classpath:${jboss.home.dir}\/providers\/*<\/provider>module:be.looorent.keycloak-configurable-token/g' /opt/jboss/keycloak/standalone/configuration/standalone.xml
88 | ```
89 |
90 | ## Response format
91 |
92 | ```javascript
93 | {
94 | "access_token": "...",
95 | "expires_in": ,
96 | "refresh_expires_in": 0,
97 | "token_type": "bearer",
98 | "not-before-policy": ...,
99 | "session_state": "...",
100 | "scope": ""
101 | }
102 | ```
103 |
104 | ## Use case
105 |
106 | ### Specify a short-lived token
107 |
108 | Request's body must be in JSON and include an attribute `tokenLifespanInSeconds` (that must be strictly positive).
109 |
110 | Example using CURL:
111 | ```
112 | $ curl -X POST -d '{ "tokenLifespanInSeconds": 20}' -H "Content-Type: application/json" -H "Authorization: Bearer " http://auth.service.io/auth/realms/a-realm/configurable-token
113 | ```
114 |
115 | ### Specify a long-lived token
116 |
117 | Request's body must be in JSON and include an attribute `tokenLifespanInSeconds` (that must be strictly positive).
118 | A very long lifespan (limited to the Java `integer` type) can be provided. For example: `31556952` means `1 year`.
119 |
120 | The exchanging token must include the realm role defined by the environment variable named `KEYCLOAK_LONG_LIVED_ROLE_NAME`, otherwise `tokenLifespanInSeconds` will be ignored. Pay attention this role must be present in the exchange token itself, not on the Keycloak user only.
121 |
122 | Example using CURL:
123 | ```
124 | $ curl -X POST -d '{ "tokenLifespanInSeconds": 63113904}' -H "Content-Type: application/json" -H "Authorization: Bearer " http://auth.service.io/auth/realms/a-realm/configurable-token
125 | ```
126 |
127 | ## Limits
128 |
129 | * Requesting a token for a different `clientId` is not available
130 |
131 | ## Development
132 |
133 | ### Build library
134 |
135 | ```bash
136 | $ ./gradlew build
137 | ```
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.keycloak_version = "26.0.0"
3 | repositories {
4 | mavenCentral()
5 | }
6 | }
7 |
8 | group "be.looorent"
9 | version "26.0.0"
10 |
11 | apply plugin: "java"
12 |
13 | sourceCompatibility = 1.21
14 |
15 | repositories {
16 | mavenCentral()
17 | }
18 |
19 | dependencies {
20 | compileOnly group: "org.keycloak", name: "keycloak-core", version: keycloak_version
21 | compileOnly group: "org.keycloak", name: "keycloak-server-spi", version: keycloak_version
22 | compileOnly group: "org.keycloak", name: "keycloak-server-spi-private", version: keycloak_version
23 | compileOnly group: "org.keycloak", name: "keycloak-services", version: keycloak_version
24 | compileOnly group: "org.jboss.spec.javax.ws.rs", name: "jboss-jaxrs-api_2.1_spec", version: "2.0.2.Final"
25 | compileOnly group: "org.jboss.logging", name: "jboss-logging", version: "3.5.3.Final"
26 | }
27 |
28 | task copyJar {
29 | copy {
30 | into "dist"
31 | from "build/libs/keycloak-configurable-token-${version}.jar"
32 | }
33 | }
34 |
35 | build.finalizedBy(copyJar)
--------------------------------------------------------------------------------
/dist/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/.gitkeep
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-0.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-0.1.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-0.2.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-0.2.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-0.3.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-0.3.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-0.4.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-0.4.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-0.5.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-0.5.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-1.0.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-1.0.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-1.1.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-1.1.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-1.10.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-1.10.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-1.2.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-1.2.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-1.3.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-1.3.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-1.5.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-1.5.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-1.5.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-1.5.1.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-1.7.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-1.7.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-1.8.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-1.8.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-1.9.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-1.9.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-21.0.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-21.0.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-21.0.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-21.0.1.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-22.0.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-22.0.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-23.0.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-23.0.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-24.0.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-24.0.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-25.0.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-25.0.0.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-25.0.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-25.0.1.jar
--------------------------------------------------------------------------------
/dist/keycloak-configurable-token-26.0.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/dist/keycloak-configurable-token-26.0.0.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/looorent/keycloak-configurable-token-api/17af43ac057c0d63b919832ce4d0d232f00fe4ca/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'keycloak-configurable-token'
2 |
3 |
--------------------------------------------------------------------------------
/src/main/java/be/looorent/ConfigurableTokenResource.java:
--------------------------------------------------------------------------------
1 | package be.looorent;
2 |
3 | import jakarta.ws.rs.*;
4 | import jakarta.ws.rs.core.MediaType;
5 | import jakarta.ws.rs.core.Response;
6 | import org.jboss.logging.Logger;
7 | import org.keycloak.TokenVerifier;
8 | import org.keycloak.common.VerificationException;
9 | import org.keycloak.crypto.SignatureProvider;
10 | import org.keycloak.crypto.SignatureVerifierContext;
11 | import org.keycloak.events.EventBuilder;
12 | import org.keycloak.http.HttpRequest;
13 | import org.keycloak.models.*;
14 | import org.keycloak.protocol.oidc.TokenManager;
15 | import org.keycloak.representations.AccessToken;
16 | import org.keycloak.representations.AccessTokenResponse;
17 | import org.keycloak.representations.idm.ErrorRepresentation;
18 | import org.keycloak.services.Urls;
19 | import org.keycloak.services.cors.Cors;
20 | import org.keycloak.services.managers.AppAuthManager;
21 | import org.keycloak.services.managers.AuthenticationManager;
22 |
23 | import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
24 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
25 | import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
26 | import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
27 | import static jakarta.ws.rs.core.Response.ok;
28 | import static java.util.Optional.ofNullable;
29 | import static org.keycloak.services.cors.Cors.ACCESS_CONTROL_ALLOW_METHODS;
30 | import static org.keycloak.services.cors.Cors.ACCESS_CONTROL_ALLOW_ORIGIN;
31 | import static org.keycloak.services.util.DefaultClientSessionContext.fromClientSessionScopeParameter;
32 |
33 | /**
34 | * @author Lorent Lempereur
35 | */
36 | public class ConfigurableTokenResource {
37 |
38 | private static final Logger LOG = Logger.getLogger(ConfigurableTokenResource.class);
39 |
40 | private final KeycloakSession session;
41 | private final TokenManager tokenManager;
42 | private final EventBuilder event;
43 | private final ConfigurationTokenResourceConfiguration configuration;
44 |
45 | ConfigurableTokenResource(KeycloakSession session,
46 | EventBuilder event,
47 | ConfigurationTokenResourceConfiguration configuration) {
48 | this.session = session;
49 | this.event = event;
50 | this.tokenManager = new TokenManager();
51 | this.configuration = configuration;
52 | }
53 |
54 | @Path("")
55 | @OPTIONS
56 | public Response preflight() {
57 | return session.getProvider(Cors.class)
58 | .auth()
59 | .preflight()
60 | .allowedMethods("POST", "OPTIONS")
61 | .allowedOrigins(configuration.getCorsOrigins())
62 | .add(ok());
63 | }
64 |
65 | @POST
66 | @Consumes(APPLICATION_JSON)
67 | @Produces(APPLICATION_JSON)
68 | public Response createToken(TokenConfiguration tokenConfiguration) {
69 | HttpRequest request = session.getContext().getHttpRequest();
70 | try {
71 | AccessToken accessToken = validateTokenAndUpdateSession(request);
72 | UserSessionModel userSession = this.findSession();
73 | AccessTokenResponse response = this.createAccessToken(userSession, accessToken, tokenConfiguration);
74 | return this.buildCorsResponse(request, response);
75 | } catch (ConfigurableTokenException e) {
76 | LOG.error("An error occurred when fetching an access token", e);
77 | ErrorRepresentation error = new ErrorRepresentation();
78 | error.setErrorMessage(e.getMessage());
79 | return buildCorsResponse(Response.status(BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON));
80 | }
81 | }
82 |
83 | private AccessTokenResponse createAccessToken(UserSessionModel userSession,
84 | AccessToken accessToken,
85 | TokenConfiguration tokenConfiguration) {
86 | RealmModel realm = this.session.getContext().getRealm();
87 | ClientModel client = realm.getClientByClientId(accessToken.getIssuedFor());
88 | LOG.infof("Configurable token requested for username=%s and client=%s on realm=%s", userSession.getUser().getUsername(), client.getClientId(), realm.getName());
89 | AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
90 | ClientSessionContext clientSessionContext = fromClientSessionScopeParameter(clientSession, session);
91 | session.getContext().setClient(client);
92 |
93 | AccessToken newToken = tokenManager.createClientAccessToken(session, realm, client, userSession.getUser(), userSession, clientSessionContext);
94 | updateTokenExpiration(newToken, tokenConfiguration, userSession.getUser());
95 | return buildResponse(realm, userSession, client, clientSession, newToken);
96 | }
97 |
98 | private AccessToken validateTokenAndUpdateSession(HttpRequest request) throws ConfigurableTokenException {
99 | try {
100 | RealmModel realm = session.getContext().getRealm();
101 | String tokenString = readAccessTokenFrom(request);
102 | TokenVerifier verifier = TokenVerifier.create(tokenString, AccessToken.class).withChecks(
103 | TokenVerifier.IS_ACTIVE,
104 | new TokenVerifier.RealmUrlCheck(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()))
105 | );
106 | SignatureVerifierContext verifierContext = session.getProvider(SignatureProvider.class, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId());
107 | verifier.verifierContext(verifierContext);
108 | AccessToken accessToken = verifier.verify().getToken();
109 | if (tokenManager.checkTokenValidForIntrospection(session, realm, accessToken, event) == null) {
110 | throw new VerificationException("introspection_failed");
111 | }
112 | return accessToken;
113 | } catch (VerificationException e) {
114 | LOG.warn("Keycloak-ConfigurableToken: introspection of token failed", e);
115 | throw new ConfigurableTokenException("access_token_introspection_failed: "+e.getMessage());
116 | }
117 | }
118 |
119 | private String readAccessTokenFrom(HttpRequest request) throws ConfigurableTokenException {
120 | String authorization = request.getHttpHeaders().getHeaderString(AUTHORIZATION);
121 | if (authorization == null || !authorization.startsWith("Bearer ")) {
122 | LOG.warn("Keycloak-ConfigurableToken: no authorization header with bearer token");
123 | throw new ConfigurableTokenException("bearer_token_missing_in_authorization_header");
124 | }
125 | String token = authorization.substring(7);
126 | if (token.isEmpty()) {
127 | LOG.warn("Keycloak-ConfigurableToken: empty access token");
128 | throw new ConfigurableTokenException("missing_access_token");
129 | }
130 | return token;
131 | }
132 |
133 | private UserSessionModel findSession() throws ConfigurableTokenException {
134 | AuthenticationManager.AuthResult authenticated = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();
135 |
136 | if (authenticated == null) {
137 | LOG.warn("Keycloak-ConfigurableToken: user not authenticated");
138 | throw new ConfigurableTokenException("not_authenticated");
139 | }
140 |
141 | if (authenticated.getToken().getRealmAccess() == null) {
142 | LOG.warn("Keycloak-ConfigurableToken: no realm associated with authorization");
143 | throw new ConfigurableTokenException("wrong_realm");
144 | }
145 |
146 | UserModel user = authenticated.getUser();
147 | if (user == null || !user.isEnabled()) {
148 | LOG.warn("Keycloak-ConfigurableToken: user does not exist or is not enabled");
149 | throw new ConfigurableTokenException("invalid_user");
150 | }
151 |
152 | UserSessionModel userSession = authenticated.getSession();
153 | if (userSession == null) {
154 | LOG.warn("Keycloak-ConfigurableToken: user does not have any active session");
155 | throw new ConfigurableTokenException("missing_user_session");
156 | }
157 |
158 | return userSession;
159 | }
160 |
161 | private Response buildCorsResponse(HttpRequest request, AccessTokenResponse response) {
162 | return buildCorsResponse(ok(response).type(APPLICATION_JSON_TYPE));
163 | }
164 |
165 | private Response buildCorsResponse(Response.ResponseBuilder responseBuilder) {
166 | return session.getProvider(Cors.class)
167 | .auth()
168 | .allowedMethods("POST", "OPTIONS")
169 | .auth()
170 | .exposedHeaders(ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN)
171 | .allowedOrigins(configuration.getCorsOrigins())
172 | .add(responseBuilder);
173 | }
174 |
175 | private AccessTokenResponse buildResponse(RealmModel realm,
176 | UserSessionModel userSession,
177 | ClientModel client,
178 | AuthenticatedClientSessionModel clientSession,
179 | AccessToken token) {
180 | EventBuilder eventBuilder = new EventBuilder(realm, session, session.getContext().getConnection());
181 | ClientSessionContext clientSessionContext = fromClientSessionScopeParameter(clientSession, session);
182 | return tokenManager.responseBuilder(realm, client, eventBuilder, session, userSession, clientSessionContext)
183 | .accessToken(token)
184 | .build();
185 | }
186 |
187 | private void updateTokenExpiration(AccessToken token, TokenConfiguration tokenConfiguration, UserModel user) {
188 | boolean longLivedTokenAllowed = ofNullable(session.getContext().getRealm().getRole(this.configuration.getLongLivedTokenRole()))
189 | .map(user::hasRole)
190 | .orElse(false);
191 | token.exp((long) tokenConfiguration.computeTokenExpiration(token.getExp(), longLivedTokenAllowed));
192 | }
193 |
194 | static class ConfigurableTokenException extends Exception {
195 | public ConfigurableTokenException(String message) {
196 | super(message);
197 | }
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/main/java/be/looorent/ConfigurableTokenResourceProvider.java:
--------------------------------------------------------------------------------
1 | package be.looorent;
2 |
3 | import org.keycloak.events.EventBuilder;
4 | import org.keycloak.models.KeycloakSession;
5 | import org.keycloak.services.resource.RealmResourceProvider;
6 |
7 | /**
8 | * @author Lorent Lempereur
9 | */
10 | public class ConfigurableTokenResourceProvider implements RealmResourceProvider {
11 | private final KeycloakSession session;
12 | private final EventBuilder eventBuilder;
13 | private final ConfigurationTokenResourceConfiguration configuration;
14 |
15 | ConfigurableTokenResourceProvider(KeycloakSession session,
16 | EventBuilder eventBuilder,
17 | ConfigurationTokenResourceConfiguration configuration) {
18 | this.session = session;
19 | this.eventBuilder = eventBuilder;
20 | this.configuration = configuration;
21 | }
22 |
23 | @Override
24 | public Object getResource() {
25 | return new ConfigurableTokenResource(session, eventBuilder, configuration);
26 | }
27 |
28 | @Override
29 | public void close() {}
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/be/looorent/ConfigurableTokenResourceProviderFactory.java:
--------------------------------------------------------------------------------
1 | package be.looorent;
2 |
3 | import org.jboss.logging.Logger;
4 | import org.keycloak.Config;
5 | import org.keycloak.events.EventBuilder;
6 | import org.keycloak.models.KeycloakSession;
7 | import org.keycloak.models.KeycloakSessionFactory;
8 | import org.keycloak.services.resource.RealmResourceProvider;
9 | import org.keycloak.services.resource.RealmResourceProviderFactory;
10 |
11 | import static be.looorent.ConfigurationTokenResourceConfiguration.readFromEnvironment;
12 |
13 | /**
14 | * @author Lorent Lempereur
15 | */
16 | public class ConfigurableTokenResourceProviderFactory implements RealmResourceProviderFactory {
17 | static final String ID = "configurable-token";
18 | private static final Logger LOG = Logger.getLogger(ConfigurableTokenResourceProviderFactory.class);
19 |
20 | @Override
21 | public RealmResourceProvider create(KeycloakSession session) {
22 | ConfigurationTokenResourceConfiguration configuration = readFromEnvironment();
23 | LOG.infof("Keycloak-ConfigurableToken is configured with: %s", configuration);
24 | EventBuilder eventBuilder = new EventBuilder(session.getContext().getRealm(), session);
25 | return new ConfigurableTokenResourceProvider(session, eventBuilder, configuration);
26 | }
27 |
28 | @Override
29 | public void init(Config.Scope config) {}
30 |
31 | @Override
32 | public void postInit(KeycloakSessionFactory factory) {}
33 |
34 | @Override
35 | public void close() {}
36 |
37 | @Override
38 | public String getId() {
39 | return ID;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/be/looorent/ConfigurationTokenResourceConfiguration.java:
--------------------------------------------------------------------------------
1 | package be.looorent;
2 |
3 | import org.jboss.logging.Logger;
4 |
5 | import static java.lang.System.getenv;
6 | import static java.util.Arrays.asList;
7 |
8 | /**
9 | * Configures this API.
10 | * @author Lorent Lempereur
11 | */
12 | public class ConfigurationTokenResourceConfiguration {
13 | private static final Logger LOG = Logger.getLogger(ConfigurableTokenResourceProviderFactory.class);
14 |
15 | private static final String KEYCLOAK_LONG_LIVED_ROLE_NAME = "KEYCLOAK_LONG_LIVED_ROLE_NAME";
16 | private static final String DEFAULT_KEYCLOAK_LONG_LIVED_ROLE_NAME = "long_lived_token";
17 | private static final String KEYCLOAK_LONG_LIVED_CORS_ORIGINS = "KEYCLOAK_LONG_LIVED_CORS_ORIGINS";
18 | private static final String[] DEFAULT_KEYCLOAK_LONG_LIVED_CORS_ORIGINS = new String[] { "*" };
19 |
20 | private final String longLivedTokenRole;
21 | private final String[] corsOrigins;
22 |
23 | public static ConfigurationTokenResourceConfiguration readFromEnvironment() {
24 | String longLivedTokenRole = readLongLivedRoleFromEnvironment();
25 | String[] corsOrigins = readCorsOrigins();
26 | return new ConfigurationTokenResourceConfiguration(longLivedTokenRole, corsOrigins);
27 | }
28 |
29 | public ConfigurationTokenResourceConfiguration(String longLivedTokenRole, String[] corsOrigins) {
30 | this.longLivedTokenRole = longLivedTokenRole;
31 | this.corsOrigins = corsOrigins;
32 | }
33 |
34 | public String getLongLivedTokenRole() {
35 | return longLivedTokenRole;
36 | }
37 |
38 | @Override
39 | public String toString() {
40 | return "longLivedTokenRole=" + longLivedTokenRole;
41 | }
42 |
43 | public String[] getCorsOrigins() {
44 | return corsOrigins;
45 | }
46 |
47 | private static String readLongLivedRoleFromEnvironment() {
48 | String roleForLongLivedTokens = getenv(KEYCLOAK_LONG_LIVED_ROLE_NAME);
49 | if (roleForLongLivedTokens == null || roleForLongLivedTokens.trim().isEmpty()) {
50 | LOG.warn("Keycloak-ConfigurableToken : Long lived role name provided, using default one.");
51 | return DEFAULT_KEYCLOAK_LONG_LIVED_ROLE_NAME;
52 | } else {
53 | return roleForLongLivedTokens;
54 | }
55 | }
56 |
57 | private static String[] readCorsOrigins() {
58 | String corsOrigins = getenv(KEYCLOAK_LONG_LIVED_CORS_ORIGINS);
59 | if (corsOrigins == null || corsOrigins.trim().isEmpty()) {
60 | LOG.warnf("Keycloak-ConfigurableToken : no cors origin is defined in environment variables. Using '%s'.", asList(DEFAULT_KEYCLOAK_LONG_LIVED_CORS_ORIGINS));
61 | return DEFAULT_KEYCLOAK_LONG_LIVED_CORS_ORIGINS;
62 | } else {
63 | String[] origins = parseCorsAllowedOrigins(corsOrigins);
64 | LOG.debugf("Keycloak-LoginActionToken : CORS origin allowed (for sessions) configured: %s", asList(origins));
65 | return origins;
66 | }
67 | }
68 |
69 | private static String[] parseCorsAllowedOrigins(String corsOrigins) {
70 | return corsOrigins.trim().split(",");
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/be/looorent/TokenConfiguration.java:
--------------------------------------------------------------------------------
1 | package be.looorent;
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator;
4 | import com.fasterxml.jackson.annotation.JsonProperty;
5 |
6 | import static java.lang.Math.min;
7 | import static java.util.Optional.ofNullable;
8 | import static org.keycloak.common.util.Time.currentTime;
9 |
10 | /**
11 | * @author Lorent Lempereur
12 | */
13 | public class TokenConfiguration {
14 |
15 | @JsonProperty("tokenLifespanInSeconds")
16 | private final Integer tokenLifespanInSeconds;
17 |
18 | @JsonCreator
19 | public TokenConfiguration(@JsonProperty("tokenLifespanInSeconds") Integer tokenLifespanInSeconds) {
20 | this.tokenLifespanInSeconds = tokenLifespanInSeconds;
21 | }
22 |
23 | public Integer getTokenLifespanInSeconds() {
24 | return tokenLifespanInSeconds;
25 | }
26 |
27 | public int computeTokenExpiration(Long maxExpiration, boolean longLivedTokenAllowed) {
28 | int furthestExpiration = ofNullable(maxExpiration).map(Long::intValue).orElse(0);
29 | return ofNullable(getTokenLifespanInSeconds())
30 | .map(lifespan -> currentTime() + lifespan)
31 | .map(requestedExpiration -> longLivedTokenAllowed ? requestedExpiration : min(furthestExpiration, requestedExpiration))
32 | .orElse(furthestExpiration);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/jboss-deployment-structure.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory:
--------------------------------------------------------------------------------
1 | be.looorent.ConfigurableTokenResourceProviderFactory
--------------------------------------------------------------------------------