Skip to content

Commit ae5d690

Browse files
sberyozkinDavideD
authored andcommitted
Record access token scope in the grant response
1 parent 4ce957a commit ae5d690

File tree

19 files changed

+161
-52
lines changed

19 files changed

+161
-52
lines changed

extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerProcessor.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,29 +44,31 @@ SyntheticBeanBuildItem produceDbTokenStateManagerBean(OidcDbTokenStateManagerRec
4444
final String[] queryParamPlaceholders;
4545
switch (sqlClientBuildItem.reactiveClient) {
4646
case REACTIVE_PG_CLIENT:
47-
queryParamPlaceholders = new String[] { "$1", "$2", "$3", "$4", "$5", "$6" };
47+
queryParamPlaceholders = new String[] { "$1", "$2", "$3", "$4", "$5", "$6", "$7" };
4848
break;
4949
case REACTIVE_MSSQL_CLIENT:
50-
queryParamPlaceholders = new String[] { "@p1", "@p2", "@p3", "@p4", "@p5", "@p6" };
50+
queryParamPlaceholders = new String[] { "@p1", "@p2", "@p3", "@p4", "@p5", "@p6", "@p7" };
5151
break;
5252
case REACTIVE_MYSQL_CLIENT:
5353
case REACTIVE_DB2_CLIENT:
5454
case REACTIVE_ORACLE_CLIENT:
55-
queryParamPlaceholders = new String[] { "?", "?", "?", "?", "?", "?" };
55+
queryParamPlaceholders = new String[] { "?", "?", "?", "?", "?", "?", "?" };
5656
break;
5757
default:
5858
throw new RuntimeException("Unknown Reactive Sql Client " + sqlClientBuildItem.reactiveClient);
5959
}
6060
String deleteStatement = format("DELETE FROM oidc_db_token_state_manager WHERE id = %s", queryParamPlaceholders[0]);
6161
String getQuery = format(
62-
"SELECT id_token, access_token, refresh_token, access_token_expires_in FROM oidc_db_token_state_manager WHERE "
62+
"SELECT id_token, access_token, refresh_token, access_token_expires_in, access_token_scope FROM oidc_db_token_state_manager WHERE "
6363
+
6464
"id = %s",
6565
queryParamPlaceholders[0]);
6666
String insertStatement = format("INSERT INTO oidc_db_token_state_manager (id_token, access_token, refresh_token," +
67-
" access_token_expires_in, expires_in, id) VALUES (%s, %s, %s, %s, %s, %s)", queryParamPlaceholders[0],
67+
" access_token_expires_in, access_token_scope, expires_in, id) VALUES (%s, %s, %s, %s, %s, %s, %s)",
68+
queryParamPlaceholders[0],
6869
queryParamPlaceholders[1],
69-
queryParamPlaceholders[2], queryParamPlaceholders[3], queryParamPlaceholders[4], queryParamPlaceholders[5]);
70+
queryParamPlaceholders[2], queryParamPlaceholders[3], queryParamPlaceholders[4], queryParamPlaceholders[5],
71+
queryParamPlaceholders[6]);
7072
return SyntheticBeanBuildItem
7173
.configure(OidcDbTokenStateManager.class)
7274
.alternative(true)
@@ -119,6 +121,7 @@ SyntheticBeanBuildItem createDbTokenStateInitializerProps(ReactiveSqlClientBuild
119121
"access_token VARCHAR, " +
120122
"refresh_token VARCHAR, " +
121123
"access_token_expires_in BIGINT, " +
124+
"access_token_scope VARCHAR, " +
122125
"expires_in BIGINT NOT NULL)";
123126
supportsIfTableNotExists = true;
124127
break;
@@ -129,6 +132,7 @@ SyntheticBeanBuildItem createDbTokenStateInitializerProps(ReactiveSqlClientBuild
129132
+ "access_token VARCHAR(5000) NULL, "
130133
+ "refresh_token VARCHAR(5000) NULL, "
131134
+ "access_token_expires_in BIGINT NULL, "
135+
+ "access_token_scope VARCHAR(100) NULL, "
132136
+ "expires_in BIGINT NOT NULL, "
133137
+ "PRIMARY KEY (id))";
134138
supportsIfTableNotExists = true;
@@ -140,6 +144,7 @@ SyntheticBeanBuildItem createDbTokenStateInitializerProps(ReactiveSqlClientBuild
140144
+ "access_token NVARCHAR(MAX), "
141145
+ "refresh_token NVARCHAR(MAX), "
142146
+ "access_token_expires_in BIGINT, "
147+
+ "access_token_scope NVARCHAR(100), "
143148
+ "expires_in BIGINT NOT NULL)";
144149
supportsIfTableNotExists = false;
145150
break;
@@ -150,6 +155,7 @@ SyntheticBeanBuildItem createDbTokenStateInitializerProps(ReactiveSqlClientBuild
150155
+ "access_token VARCHAR(4000), "
151156
+ "refresh_token VARCHAR(4000), "
152157
+ "access_token_expires_in BIGINT, "
158+
+ "access_token_scope VARCHAR(100), "
153159
+ "expires_in BIGINT NOT NULL)";
154160
supportsIfTableNotExists = false;
155161
break;
@@ -160,6 +166,7 @@ SyntheticBeanBuildItem createDbTokenStateInitializerProps(ReactiveSqlClientBuild
160166
+ "access_token VARCHAR2(4000), "
161167
+ "refresh_token VARCHAR2(4000), "
162168
+ "access_token_expires_in NUMBER, "
169+
+ "access_token_scope VARCHAR2(100), "
163170
+ "expires_in NUMBER NOT NULL, "
164171
+ "PRIMARY KEY (id))";
165172
supportsIfTableNotExists = true;

extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ public void testCodeFlow() throws IOException {
7070

7171
textPage = loginForm.getButtonByName("login").click();
7272

73-
assertEquals("alice, access token: true, access_token_expires_in: true, refresh_token: true",
73+
assertEquals(
74+
"alice, access token: true, access_token_expires_in: true, access_token_scope: true, refresh_token: true",
7475
textPage.getContent());
7576

7677
assertTokenStateCount(1);

extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerEntity.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public class OidcDbTokenStateManagerEntity {
2424
@Column(name = "access_token_expires_in")
2525
Long accessTokenExpiresIn;
2626

27+
@Column(name = "access_token_scope", length = 100)
28+
String accessTokenScope;
29+
2730
@Column(name = "expires_in")
2831
Long expiresIn;
2932
}

extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/ProtectedResource.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public String getName() {
2828
return idToken.getName()
2929
+ ", access token: " + (tokens.getAccessToken() != null)
3030
+ ", access_token_expires_in: " + (tokens.getAccessTokenExpiresIn() != null)
31+
+ ", access_token_scope: " + (tokens.getAccessTokenScope() != null)
3132
+ ", refresh_token: " + (tokens.getRefreshToken() != null);
3233

3334
}

extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManager.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class OidcDbTokenStateManager implements TokenStateManager {
3131
private static final String ID_TOKEN_COLUMN = "id_token";
3232
private static final String ACCESS_TOKEN_COLUMN = "access_token";
3333
private static final String ACCESS_TOKEN_EXPIRES_IN_COLUMN = "access_token_expires_in";
34+
private static final String ACCESS_TOKEN_SCOPE_COLUMN = "access_token_scope";
3435
private static final String REFRESH_TOKEN_COLUMN = "refresh_token";
3536

3637
private final String insertStatement;
@@ -61,6 +62,7 @@ public Uni<String> createTokenState(RoutingContext event, OidcTenantConfig oidcC
6162
.execute(
6263
Tuple.of(tokens.getIdToken(), tokens.getAccessToken(),
6364
tokens.getRefreshToken(), tokens.getAccessTokenExpiresIn(),
65+
tokens.getAccessTokenScope(),
6466
expiresIn(event), id)))
6567
.toCompletionStage())
6668
.onFailure().transform(new Function<Throwable, Throwable>() {
@@ -110,7 +112,8 @@ public Uni<? extends AuthorizationCodeTokens> apply(RowSet<Row> rows) {
110112
firstRow.getString(ID_TOKEN_COLUMN),
111113
firstRow.getString(ACCESS_TOKEN_COLUMN),
112114
firstRow.getString(REFRESH_TOKEN_COLUMN),
113-
firstRow.getLong(ACCESS_TOKEN_EXPIRES_IN_COLUMN)));
115+
firstRow.getLong(ACCESS_TOKEN_EXPIRES_IN_COLUMN),
116+
firstRow.getString(ACCESS_TOKEN_SCOPE_COLUMN)));
114117
}
115118
}
116119
return Uni.createFrom().failure(new AuthenticationCompletionException(FAILED_TO_ACQUIRE_TOKEN));

extensions/oidc-redis-token-state-manager/deployment/src/test/java/io/quarkus/oidc/redis/token/state/manager/deployment/AbstractRedisTokenStateManagerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ public void testCodeFlow() throws IOException {
5959

6060
textPage = loginForm.getButtonByName("login").click();
6161

62-
assertEquals("alice, access token: true, access_token_expires_in: true, refresh_token: true",
62+
assertEquals(
63+
"alice, access token: true, access_token_expires_in: true, access_token_scope: true, refresh_token: true",
6364
textPage.getContent());
6465

6566
assertTokenStateCount(1);

extensions/oidc-redis-token-state-manager/deployment/src/test/java/io/quarkus/oidc/redis/token/state/manager/deployment/ProtectedResource.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public String getName() {
2828
return idToken.getName()
2929
+ ", access token: " + (tokens.getAccessToken() != null)
3030
+ ", access_token_expires_in: " + (tokens.getAccessTokenExpiresIn() != null)
31+
+ ", access_token_scope: " + (tokens.getAccessTokenScope() != null)
3132
+ ", refresh_token: " + (tokens.getRefreshToken() != null);
3233
}
3334

extensions/oidc-redis-token-state-manager/runtime/src/main/java/io/quarkus/oidc/redis/token/state/manager/runtime/OidcRedisTokenStateManager.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,16 @@ private static Instant expiresAt(RoutingContext event) {
8181
return Instant.now().plusSeconds(event.<Long> get(SESSION_MAX_AGE_PARAM));
8282
}
8383

84-
record AuthorizationCodeTokensRecord(String idToken, String accessToken, String refreshToken, Long accessTokenExpiresIn) {
84+
record AuthorizationCodeTokensRecord(String idToken, String accessToken, String refreshToken, Long accessTokenExpiresIn,
85+
String accessTokenScope) {
8586

8687
private static AuthorizationCodeTokensRecord of(AuthorizationCodeTokens tokens) {
8788
return new AuthorizationCodeTokensRecord(tokens.getIdToken(), tokens.getAccessToken(), tokens.getRefreshToken(),
88-
tokens.getAccessTokenExpiresIn());
89+
tokens.getAccessTokenExpiresIn(), tokens.getAccessTokenScope());
8990
}
9091

9192
private AuthorizationCodeTokens toTokens() {
92-
return new AuthorizationCodeTokens(idToken, accessToken, refreshToken, accessTokenExpiresIn);
93+
return new AuthorizationCodeTokens(idToken, accessToken, refreshToken, accessTokenExpiresIn, accessTokenScope);
9394
}
9495
}
9596
}

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AuthorizationCodeTokens.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class AuthorizationCodeTokens {
99
private String accessToken;
1010
private String refreshToken;
1111
private Long accessTokenExpiresIn;
12+
private String accessTokenScope;
1213

1314
public AuthorizationCodeTokens() {
1415
}
@@ -18,10 +19,16 @@ public AuthorizationCodeTokens(String idToken, String accessToken, String refres
1819
}
1920

2021
public AuthorizationCodeTokens(String idToken, String accessToken, String refreshToken, Long accessTokenExpiresIn) {
22+
this(idToken, accessToken, refreshToken, accessTokenExpiresIn, null);
23+
}
24+
25+
public AuthorizationCodeTokens(String idToken, String accessToken, String refreshToken, Long accessTokenExpiresIn,
26+
String accessTokenScope) {
2127
this.idToken = idToken;
2228
this.accessToken = accessToken;
2329
this.refreshToken = refreshToken;
2430
this.accessTokenExpiresIn = accessTokenExpiresIn;
31+
this.accessTokenScope = accessTokenScope;
2532
}
2633

2734
/**
@@ -97,4 +104,22 @@ public Long getAccessTokenExpiresIn() {
97104
public void setAccessTokenExpiresIn(Long accessTokenExpiresIn) {
98105
this.accessTokenExpiresIn = accessTokenExpiresIn;
99106
}
107+
108+
/**
109+
* Get the access token scope.
110+
*
111+
* @return access token scope.
112+
*/
113+
public String getAccessTokenScope() {
114+
return accessTokenScope;
115+
}
116+
117+
/**
118+
* Set the access token scope.
119+
*
120+
* @param accessTokenScope access token scope.
121+
*/
122+
public void setAccessTokenScope(String accessTokenScope) {
123+
this.accessTokenScope = accessTokenScope;
124+
}
100125
}

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.quarkus.oidc.runtime;
22

3+
import java.nio.charset.StandardCharsets;
4+
35
import jakarta.enterprise.context.ApplicationScoped;
46

57
import org.jboss.logging.Logger;
@@ -8,6 +10,7 @@
810
import io.quarkus.oidc.OidcRequestContext;
911
import io.quarkus.oidc.OidcTenantConfig;
1012
import io.quarkus.oidc.TokenStateManager;
13+
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
1114
import io.quarkus.oidc.runtime.OidcTenantConfig.TokenStateManager.Strategy;
1215
import io.quarkus.security.AuthenticationFailedException;
1316
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
@@ -41,16 +44,21 @@ public Uni<String> createTokenState(RoutingContext routingContext, OidcTenantCon
4144
.append(CodeAuthenticationMechanism.COOKIE_DELIM)
4245
.append(tokens.getAccessTokenExpiresIn() != null ? tokens.getAccessTokenExpiresIn() : "")
4346
.append(CodeAuthenticationMechanism.COOKIE_DELIM)
47+
.append(tokens.getAccessTokenScope() != null ? encodeScopes(oidcConfig, tokens.getAccessTokenScope())
48+
: "")
49+
.append(CodeAuthenticationMechanism.COOKIE_DELIM)
4450
.append(tokens.getRefreshToken());
4551
} else if (oidcConfig.tokenStateManager().strategy() == Strategy.ID_REFRESH_TOKENS) {
4652
// But sometimes the access token is not required.
4753
// For example, when the Quarkus endpoint does not need to use it to access another service.
48-
// Skip access token and access token expiry, add refresh token
54+
// Skip access token, access token expiry, access token scope, add refresh token
4955
sb.append(CodeAuthenticationMechanism.COOKIE_DELIM)
5056
.append("")
5157
.append(CodeAuthenticationMechanism.COOKIE_DELIM)
5258
.append("")
5359
.append(CodeAuthenticationMechanism.COOKIE_DELIM)
60+
.append("")
61+
.append(CodeAuthenticationMechanism.COOKIE_DELIM)
5462
.append(tokens.getRefreshToken());
5563
}
5664

@@ -71,7 +79,9 @@ public Uni<String> createTokenState(RoutingContext routingContext, OidcTenantCon
7179
// Add access token and its expires_in property
7280
sb.append(tokens.getAccessToken())
7381
.append(CodeAuthenticationMechanism.COOKIE_DELIM)
74-
.append(tokens.getAccessTokenExpiresIn() != null ? tokens.getAccessTokenExpiresIn() : "");
82+
.append(tokens.getAccessTokenExpiresIn() != null ? tokens.getAccessTokenExpiresIn() : "")
83+
.append(CodeAuthenticationMechanism.COOKIE_DELIM)
84+
.append(tokens.getAccessTokenScope() != null ? tokens.getAccessTokenScope() : "");
7585

7686
// Encrypt access token and create a `q_session_at` cookie.
7787
CodeAuthenticationMechanism.createCookie(routingContext,
@@ -111,6 +121,7 @@ public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, Oid
111121
String idToken = null;
112122
String accessToken = null;
113123
Long accessTokenExpiresIn = null;
124+
String accessTokenScope = null;
114125
String refreshToken = null;
115126

116127
if (!oidcConfig.tokenStateManager().splitTokens()) {
@@ -122,15 +133,14 @@ public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, Oid
122133

123134
try {
124135
idToken = tokens[0];
125-
accessToken = null;
126-
refreshToken = null;
127136

128137
if (oidcConfig.tokenStateManager().strategy() == Strategy.KEEP_ALL_TOKENS) {
129138
accessToken = tokens[1];
130139
accessTokenExpiresIn = tokens[2].isEmpty() ? null : parseAccessTokenExpiresIn(tokens[2]);
131-
refreshToken = tokens[3];
140+
accessTokenScope = tokens[3].isEmpty() ? null : tokens[3];
141+
refreshToken = tokens[4];
132142
} else if (oidcConfig.tokenStateManager().strategy() == Strategy.ID_REFRESH_TOKENS) {
133-
refreshToken = tokens[3];
143+
refreshToken = tokens[4];
134144
}
135145
} catch (ArrayIndexOutOfBoundsException ex) {
136146
final String error = "Session cookie is malformed";
@@ -142,8 +152,6 @@ public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, Oid
142152
} else {
143153
// Decrypt ID token from the q_session cookie
144154
idToken = decryptToken(tokenState, routingContext, oidcConfig);
145-
accessToken = null;
146-
refreshToken = null;
147155

148156
if (oidcConfig.tokenStateManager().strategy() == Strategy.KEEP_ALL_TOKENS) {
149157
Cookie atCookie = getAccessTokenCookie(routingContext, oidcConfig);
@@ -155,6 +163,10 @@ public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, Oid
155163
try {
156164
accessTokenExpiresIn = accessTokenData[1].isEmpty() ? null
157165
: parseAccessTokenExpiresIn(accessTokenData[1]);
166+
if (accessTokenData.length == 3) {
167+
accessTokenScope = accessTokenData[2].isEmpty() ? null
168+
: decodeScopes(oidcConfig, accessTokenData[2]);
169+
}
158170
} catch (ArrayIndexOutOfBoundsException ex) {
159171
final String error = "Session cookie is malformed";
160172
LOG.debug(ex);
@@ -176,7 +188,8 @@ public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, Oid
176188
}
177189
}
178190
}
179-
return Uni.createFrom().item(new AuthorizationCodeTokens(idToken, accessToken, refreshToken, accessTokenExpiresIn));
191+
return Uni.createFrom()
192+
.item(new AuthorizationCodeTokens(idToken, accessToken, refreshToken, accessTokenExpiresIn, accessTokenScope));
180193
}
181194

182195
@Override
@@ -222,7 +235,7 @@ private static String getRefreshTokenCookieName(OidcTenantConfig oidcConfig) {
222235
return OidcUtils.SESSION_RT_COOKIE_NAME + cookieSuffix;
223236
}
224237

225-
private String encryptToken(String token, RoutingContext context, OidcTenantConfig oidcConfig) {
238+
private static String encryptToken(String token, RoutingContext context, OidcTenantConfig oidcConfig) {
226239
if (oidcConfig.tokenStateManager().encryptionRequired()) {
227240
TenantConfigContext configContext = context.get(TenantConfigContext.class.getName());
228241
try {
@@ -236,7 +249,7 @@ private String encryptToken(String token, RoutingContext context, OidcTenantConf
236249
return token;
237250
}
238251

239-
private String decryptToken(String token, RoutingContext context, OidcTenantConfig oidcConfig) {
252+
private static String decryptToken(String token, RoutingContext context, OidcTenantConfig oidcConfig) {
240253
if (oidcConfig.tokenStateManager().encryptionRequired()) {
241254
TenantConfigContext configContext = context.get(TenantConfigContext.class.getName());
242255
try {
@@ -249,4 +262,18 @@ private String decryptToken(String token, RoutingContext context, OidcTenantConf
249262
}
250263
return token;
251264
}
265+
266+
private static String encodeScopes(OidcTenantConfig oidcConfig, String accessTokenScope) {
267+
if (oidcConfig.tokenStateManager().encryptionRequired()) {
268+
return accessTokenScope;
269+
}
270+
return OidcCommonUtils.base64UrlEncode(accessTokenScope.getBytes(StandardCharsets.UTF_8));
271+
}
272+
273+
private static String decodeScopes(OidcTenantConfig oidcConfig, String accessTokenScope) {
274+
if (oidcConfig.tokenStateManager().encryptionRequired()) {
275+
return accessTokenScope;
276+
}
277+
return OidcCommonUtils.base64UrlDecode(accessTokenScope);
278+
}
252279
}

0 commit comments

Comments
 (0)