Experimental feature for JWT Authorization Grant (#43624)

Closes #43444

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano 2025-10-22 15:34:33 +02:00 committed by GitHub
parent f6ac64907d
commit a25a0268de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 639 additions and 1 deletions

View File

@ -77,6 +77,8 @@ public class Profile {
TOKEN_EXCHANGE_STANDARD_V2("Standard Token Exchange version 2", Type.DEFAULT, 2),
TOKEN_EXCHANGE_EXTERNAL_INTERNAL_V2("External to Internal Token Exchange version 2", Type.EXPERIMENTAL, 2),
JWT_AUTHORIZATION_GRANT("JWT Profile for Oauth 2.0 Authorization Grant", Type.EXPERIMENTAL),
WEB_AUTHN("W3C Web Authentication (WebAuthn)", Type.DEFAULT),
CLIENT_POLICIES("Client configuration policies", Type.DEFAULT),

View File

@ -77,6 +77,9 @@ public interface OAuth2Constants {
String CLIENT_CREDENTIALS = "client_credentials";
String JWT_AUTHORIZATION_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
String ASSERTION = "assertion";
// https://tools.ietf.org/html/draft-ietf-oauth-assertions-01#page-5
String CLIENT_ASSERTION_TYPE = "client_assertion_type";
String CLIENT_ASSERTION = "client_assertion";

View File

@ -0,0 +1,22 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.broker.provider;
import org.keycloak.protocol.oidc.JWTAuthorizationGrantValidationContext;
public interface JWTAuthorizationGrantProvider {
BrokeredIdentityContext validateAuthorizationGrantAssertion(JWTAuthorizationGrantValidationContext assertion);
}

View File

@ -0,0 +1,145 @@
package org.keycloak.protocol.oidc;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.ClientModel;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import java.util.Collections;
import java.util.List;
public class JWTAuthorizationGrantValidationContext {
private final String assertion;
private final ClientModel client;
private JsonWebToken jwt;
private final String expectedAudience;
private JWSInput jws;
private final long currentTime;
public JWTAuthorizationGrantValidationContext(String assertion, ClientModel client, String expectedAudience) {
this.assertion = assertion;
this.client = client;
this.expectedAudience = expectedAudience;
this.currentTime = Time.currentTimeMillis();
}
public void validateJWTFormat() {
try {
this.jws = new JWSInput(assertion);
this.jwt = jws.readJsonContent(JsonWebToken.class);
}
catch (Exception e) {
failure("The provided assertion is not a valid JWT");
}
}
public void validateAssertionParameters() {
if (assertion == null) {
failure("Missing parameter:" + OAuth2Constants.ASSERTION);
}
}
public void validateClient() {
if (client.isPublicClient()) {
failure("Public client not allowed to use authorization grant");
}
}
public void validateTokenActive() {
JsonWebToken token = getJWT();
int allowedClockSkew = getAllowedClockSkew();
int maxExp = getMaximumExpirationTime();
long lifespan;
if (token.getExp() == null) {
failure("Token exp claim is required");
}
if (!token.isActive(allowedClockSkew)) {
failure("Token is not active");
}
lifespan = token.getExp() - currentTime;
if (token.getIat() == null) {
if (lifespan > maxExp) {
failure("Token expiration is too far in the future and iat claim not present in token");
}
} else {
if (token.getIat() - allowedClockSkew > currentTime) {
failure("Token was issued in the future");
}
lifespan = Math.min(lifespan, maxExp);
if (lifespan <= 0) {
failure("Token is not active");
}
if (currentTime > token.getIat() + maxExp) {
failure("Token was issued too far in the past to be used now");
}
}
}
public void validateAudience() {
JsonWebToken token = getJWT();
List<String> expectedAudiences = getExpectedAudiences();
if (!token.hasAnyAudience(expectedAudiences)) {
failure("Invalid token audience");
}
}
public void validateIssuer() {
if (jwt == null || jwt.getIssuer() == null) {
failure("Missing claim: " + OAuth2Constants.ISSUER);
}
}
public void validateSubject() {
if (jwt == null || jwt.getSubject() == null) {
failure("Missing claim: " + IDToken.SUBJECT);
}
}
public void failure(String errorMessage) {
throw new RuntimeException(errorMessage);
}
public JsonWebToken getJWT() {
return jwt;
}
public JWSInput getJws() {
return jws;
}
public String getIssuer() {
return jwt.getIssuer();
}
public String getSubject() {
return jwt.getSubject();
}
public String getAssertion() {
return assertion;
}
private List<String> getExpectedAudiences() {
return Collections.singletonList(expectedAudience);
}
private int getAllowedClockSkew() {
return 15;
}
private int getMaximumExpirationTime() {
return 300;
}
}

View File

@ -33,6 +33,7 @@ import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.authentication.authenticators.client.FederatedJWTClientValidator;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.JWTAuthorizationGrantProvider;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ClientAssertionIdentityProvider;
import org.keycloak.broker.provider.ExchangeExternalToken;
@ -68,6 +69,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenExchangeContext;
import org.keycloak.protocol.oidc.JWTAuthorizationGrantValidationContext;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
@ -94,7 +96,7 @@ import java.util.Optional;
/**
* @author Pedro Igor
*/
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> implements ExchangeExternalToken, ClientAssertionIdentityProvider<OIDCIdentityProviderConfig> {
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> implements ExchangeExternalToken, ClientAssertionIdentityProvider<OIDCIdentityProviderConfig>, JWTAuthorizationGrantProvider {
protected static final Logger logger = Logger.getLogger(OIDCIdentityProvider.class);
public static final String SCOPE_OPENID = "openid";
@ -1066,4 +1068,15 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return validator.validate();
}
@Override
public BrokeredIdentityContext validateAuthorizationGrantAssertion(JWTAuthorizationGrantValidationContext context) {
//TODO: proper assertion validation
BrokeredIdentityContext user = new BrokeredIdentityContext(context.getJWT().getSubject(), getConfig());
user.setUsername(context.getJWT().getSubject());
user.setIdp(this);
return user;
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.protocol.oidc.grants;
import jakarta.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.JWTAuthorizationGrantProvider;
import org.keycloak.broker.provider.UserAuthenticationIdentityProvider;
import org.keycloak.cache.AlternativeLookupProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.JWTAuthorizationGrantValidationContext;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
private static final Logger logger = Logger.getLogger(JWTAuthorizationGrantType.class);
@Override
public Response process(Context context) {
setContext(context);
String assertion = formParams.getFirst(OAuth2Constants.ASSERTION);
String expectedAudience = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName());
JWTAuthorizationGrantValidationContext authorizationGrantContext = new JWTAuthorizationGrantValidationContext(assertion, client, expectedAudience);
try {
//client must be confidential
authorizationGrantContext.validateClient();
//validate assertion claim (grant_type already validated to select the grant type)
authorizationGrantContext.validateAssertionParameters();
//validate token is JWT and is valid (the signature is validated by the idp)
authorizationGrantContext.validateJWTFormat();
authorizationGrantContext.validateTokenActive();
//mandatory claims
authorizationGrantContext.validateAudience();
authorizationGrantContext.validateIssuer();
authorizationGrantContext.validateSubject();
//select the idp using the issuer claim
String jwtIssuer = authorizationGrantContext.getIssuer();
AlternativeLookupProvider lookupProvider = context.getSession().getProvider(AlternativeLookupProvider.class);
IdentityProviderModel identityProviderModel = lookupProvider.lookupIdentityProviderFromIssuer(session, jwtIssuer);
if (identityProviderModel == null) {
throw new RuntimeException("No Identity Provider for provided issuer");
}
UserAuthenticationIdentityProvider<?> identityProvider = IdentityBrokerService.getIdentityProvider(session, identityProviderModel.getAlias());
if (!(identityProvider instanceof JWTAuthorizationGrantProvider jwtAuthorizationGrantProvider)) {
throw new RuntimeException("Identity Provider is not configured for JWT Authorization Grant");
}
//validate the JWT assertion and get the brokered identity from the idp
BrokeredIdentityContext brokeredIdentityContext = jwtAuthorizationGrantProvider.validateAuthorizationGrantAssertion(authorizationGrantContext);
if (brokeredIdentityContext == null) {
throw new RuntimeException("Error validating JWT with identity provider");
}
//user must exist in keycloak
FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(identityProviderModel.getAlias(), brokeredIdentityContext.getId(), brokeredIdentityContext.getUsername(), brokeredIdentityContext.getToken());
UserModel user = this.session.users().getUserByFederatedIdentity(realm, federatedIdentityModel);
if (user == null) {
throw new RuntimeException("User not found");
}
String scopeParam = formParams.getFirst(OAuth2Constants.SCOPE);
//TODO: scopes processing
UserSessionModel userSession = new UserSessionManager(session).createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteHost(), "authorization-grant", false, null, null);
RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);
AuthenticationSessionModel authSession = createSessionModel(rootAuthSession, user, client, scopeParam);
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(this.session, userSession, authSession);
return createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true, null);
}
catch (Exception e) {
event.detail(Details.REASON, e.getMessage());
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, e.getMessage(), Response.Status.BAD_REQUEST);
}
}
protected AuthenticationSessionModel createSessionModel(RootAuthenticationSessionModel rootAuthSession, UserModel targetUser, ClientModel client, String scope) {
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
authSession.setAuthenticatedUser(targetUser);
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
return authSession;
}
@Override
public EventType getEventType() {
return EventType.LOGIN;
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.protocol.oidc.grants;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
public class JWTAuthorizationGrantTypeFactory implements OAuth2GrantTypeFactory, EnvironmentDependentProviderFactory {
@Override
public String getId() {
return OAuth2Constants.JWT_AUTHORIZATION_GRANT;
}
@Override
public String getShortcut() {
return "ag";
}
@Override
public OAuth2GrantType create(KeycloakSession session) {
return new JWTAuthorizationGrantType();
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.JWT_AUTHORIZATION_GRANT);
}
}

View File

@ -7,3 +7,4 @@ org.keycloak.protocol.oidc.grants.TokenExchangeGrantTypeFactory
org.keycloak.protocol.oidc.grants.ciba.CibaGrantTypeFactory
org.keycloak.protocol.oidc.grants.device.DeviceGrantTypeFactory
org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory
org.keycloak.protocol.oidc.grants.JWTAuthorizationGrantTypeFactory

View File

@ -0,0 +1,214 @@
package org.keycloak.tests.oauth;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.events.EventType;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.testframework.annotations.InjectEvents;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.InjectUser;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.events.EventAssertion;
import org.keycloak.testframework.events.Events;
import org.keycloak.testframework.oauth.OAuthClient;
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfig;
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfigBuilder;
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.ManagedUser;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.realm.RealmConfigBuilder;
import org.keycloak.testframework.realm.UserConfig;
import org.keycloak.testframework.realm.UserConfigBuilder;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.tests.client.authentication.external.ClientAuthIdpServerConfig;
import org.keycloak.testsuite.util.IdentityProviderBuilder;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import java.util.UUID;
@KeycloakIntegrationTest(config = JWTAuthorizationGrantTest.JWTAuthorizationGrantServerConfig.class)
public class JWTAuthorizationGrantTest {
private static final String IDP_ALIAS = "authorization-grant-idp";
private static final String IDP_ISSUER = "authorization-grant://mytrust-domain";
@InjectOAuthIdentityProvider(config = JWTAuthorizationGrantTest.AGIdpConfig.class)
OAuthIdentityProvider identityProvider;
@InjectRealm(config = JWTAuthorizationGranthRealmConfig.class)
protected ManagedRealm realm;
@InjectUser(config = FederatedUserConfiguration.class)
ManagedUser user;
@InjectOAuthClient
OAuthClient oAuthClient;
@InjectEvents
Events events;
@Test
public void testPublicClient() {
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
oAuthClient.client("test-public");
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Public client not allowed to use authorization grant", response, events.poll());
oAuthClient.client("test-app", "test-secret");
}
@Test
public void testMissingAssertionParameter() {
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(null).send();
assertFailure("Missing parameter:" + OAuth2Constants.ASSERTION, response, events.poll());
}
@Test
public void testBadAssertionParameter() {
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest("fake-jwt").send();
assertFailure("The provided assertion is not a valid JWT", response, events.poll());
}
@Test
public void testExpiredAssertion() {
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, null));
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Token exp claim is required", response, events.poll());
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() - 1L));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Token is not active", response, events.poll());
}
@Test
public void testBadAudience() {
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", null, IDP_ISSUER, Time.currentTime() + 300L));
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Invalid token audience", response, events.poll());
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", "fake-audience", IDP_ISSUER));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Invalid token audience", response, events.poll());
}
@Test
public void testBadIssuer() {
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), null, Time.currentTime() + 300L));
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Missing claim: " + OAuth2Constants.ISSUER, response, events.poll());
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), "fake-issuer", Time.currentTime() + 300L));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("No Identity Provider for provided issuer", response, events.poll());
}
@Test
public void testBadSubject() {
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken(null, oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L));
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("Missing claim: " + IDToken.SUBJECT, response, events.poll());
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("fake-user", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L));
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertFailure("User not found", response, events.poll());
}
@Test
public void testSuccessGrant() {
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
assertSuccess("test-app", "basic-user", response);
}
protected JsonWebToken createDefaultAuthorizationGrantToken() {
return createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L);
}
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer) {
return createAuthorizationGrantToken(subject, audience, issuer, Time.currentTime() + 300L);
}
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer, Long exp) {
JsonWebToken token = new JsonWebToken();
token.id(UUID.randomUUID().toString());
token.subject(subject);
token.audience(audience);
token.issuer(issuer);
token.exp(exp);
return token;
}
public OAuthIdentityProvider getIdentityProvider() {
return identityProvider;
}
public static class AGIdpConfig implements OAuthIdentityProviderConfig {
@Override
public OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config) {
return config;
}
}
public static class JWTAuthorizationGrantServerConfig extends ClientAuthIdpServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return super.configure(config).features(Profile.Feature.JWT_AUTHORIZATION_GRANT);
}
}
public static class JWTAuthorizationGranthRealmConfig implements RealmConfig {
@Override
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
realm.addClient("test-public").publicClient(true);
realm.identityProvider(
IdentityProviderBuilder.create()
.providerId(OIDCIdentityProviderFactory.PROVIDER_ID)
.alias(IDP_ALIAS)
.setAttribute(IdentityProviderModel.ISSUER, IDP_ISSUER)
.build());
return realm;
}
}
public static class FederatedUserConfiguration implements UserConfig {
@Override
public UserConfigBuilder configure(UserConfigBuilder user) {
return user.username("basic-user").password("password").email("basic@localhost").name("First", "Last").federatedLink(IDP_ALIAS, "basic-user-id", "basic-user");
}
}
protected void assertSuccess(String expectedClientId, String username, AccessTokenResponse response) {
Assertions.assertTrue(response.isSuccess());
AccessToken accessToken = oAuthClient.parseToken(response.getAccessToken(), AccessToken.class);
Assertions.assertEquals(expectedClientId, accessToken.getIssuedFor());
Assertions.assertEquals(username, accessToken.getPreferredUsername());
}
protected void assertFailure(String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
Assertions.assertFalse(response.isSuccess());
Assertions.assertEquals("invalid_request", response.getError());
Assertions.assertEquals(expectedErrorDescription, response.getErrorDescription());
EventAssertion.assertError(event)
.type(EventType.LOGIN_ERROR)
.error("invalid_request")
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
.details("reason", expectedErrorDescription);
}
}

View File

@ -109,6 +109,14 @@ public abstract class AbstractOAuthClient<T> {
return passwordGrantRequest(username, password).send();
}
public JWTAuthorizationGrantRequest jwtAuthorizationGrantRequest(String assertion) {
return new JWTAuthorizationGrantRequest(assertion, this);
}
public AccessTokenResponse doJWTAuthorizationGrantRequest(String assertion) {
return jwtAuthorizationGrantRequest(assertion).send();
}
public AccessTokenRequest accessTokenRequest(String code) {
return new AccessTokenRequest(code, this);
}

View File

@ -0,0 +1,38 @@
package org.keycloak.testsuite.util.oauth;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.keycloak.OAuth2Constants;
import java.io.IOException;
public class JWTAuthorizationGrantRequest extends AbstractHttpPostRequest<JWTAuthorizationGrantRequest, AccessTokenResponse> {
private String assertion;
JWTAuthorizationGrantRequest(String assertion, AbstractOAuthClient<?> client) {
super(client);
this.assertion = assertion;
}
@Override
protected String getEndpoint() {
return client.getEndpoints().getToken();
}
public JWTAuthorizationGrantRequest assertion(String assertion) {
this.assertion = assertion;
return this;
}
protected void initRequest() {
parameter(OAuth2Constants.GRANT_TYPE, OAuth2Constants.JWT_AUTHORIZATION_GRANT);
parameter("assertion", assertion);
scope();
}
@Override
protected AccessTokenResponse toResponse(CloseableHttpResponse response) throws IOException {
return new AccessTokenResponse(response);
}
}