Skip to content

Commit 4e4e205

Browse files
tnorimatYutaka Obuchi
authored andcommitted
Initial commit for ID-JAG receiver, which receives ID-JAG and send back access token working as as a part of token endpoint
Signed-off-by: Yutaka Obuchi <yutaka.obuchi.sd@hitachi.com>
1 parent 33ff9f1 commit 4e4e205

17 files changed

Lines changed: 463 additions & 13 deletions

File tree

common/src/main/java/org/keycloak/common/Profile.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ public enum Feature {
162162

163163
CIMD("OAuth Client ID Metadata Document", Type.EXPERIMENTAL),
164164

165+
IDENTITY_ASSERTION_JWT_VALIDATOR("Identity Assertion JWT validator", Type.EXPERIMENTAL),
166+
165167
/**
166168
* @see <a href="https://github.com/keycloak/keycloak/issues/37967">Deprecate for removal the Instagram social broker</a>.
167169
*/

core/src/main/java/org/keycloak/OAuth2Constants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ public interface OAuth2Constants {
8080
String JWT_AUTHORIZATION_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
8181
String ASSERTION = "assertion";
8282

83+
// https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/
84+
String IDENTITY_ASSERTION_JWT_HEADER_TYPE = "oauth-id-jag+jwt";
85+
8386
// https://tools.ietf.org/html/draft-ietf-oauth-assertions-01#page-5
8487
String CLIENT_ASSERTION_TYPE = "client_assertion_type";
8588
String CLIENT_ASSERTION = "client_assertion";
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.keycloak.protocol.oidc;
2+
3+
import java.util.List;
4+
5+
import org.keycloak.provider.Provider;
6+
7+
/**
8+
* Provider interface to support pluggable validators of JWTAuthorizationGrant token.
9+
* The pluggable validators are supposed to be provided for each JWT token type
10+
*
11+
* @author <a href="mailto:yutaka.obuchi.sd@hitachi.com">Yutaka Obuchi</a>
12+
*/
13+
14+
public interface JWTAuthorizationGrantValidator extends Provider, JWTAuthorizationGrantValidationContext{
15+
16+
public void validateClient();
17+
18+
public void validateIssuer();
19+
20+
public void validateSubject();
21+
22+
public boolean validateTokenActive(int allowedClockSkew, int maxExp, boolean reusePermitted);
23+
24+
public boolean validateSignatureAlgorithm(String expectedSignatureAlg);
25+
26+
public boolean validateTokenAudience(List<String> expectedAudiences, boolean multipleAudienceAllowed);
27+
28+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.keycloak.protocol.oidc;
2+
3+
import org.keycloak.provider.ProviderFactory;
4+
5+
/**
6+
* Provider interface to support pluggable validators of JWTAuthorizationGrant token.
7+
* The pluggable validators are supposed to be provided for each JWT token type.
8+
*
9+
* @author <a href="mailto:yutaka.obuchi.sd@hitachi.com">Yutaka Obuchi</a>
10+
*/
11+
12+
public interface JWTAuthorizationGrantValidatorFactory extends ProviderFactory<JWTAuthorizationGrantValidator> {
13+
14+
/**
15+
* @return usually like 3-letters shortcut of specific grants. It can be useful for example in the tokens when the amount of characters should be limited and hence using full grant name
16+
* is not ideal. Shortcut should be unique across grants.
17+
*/
18+
String getShortcut();
19+
20+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.keycloak.protocol.oidc;
2+
3+
import org.keycloak.provider.Provider;
4+
import org.keycloak.provider.ProviderFactory;
5+
import org.keycloak.provider.Spi;
6+
7+
/**
8+
* <p>A {@link Spi} support pluggable validators of JWTAuthorizationGrant token
9+
* The pluggable validators are supposed to be provided for each JWT token type
10+
*
11+
* @author <a href="mailto:yutaka.obuchi.sd@hitachi.com">Yutaka Obuchi</a>
12+
*/
13+
14+
public class JWTAuthorizationGrantValidatorSpi implements Spi {
15+
16+
public static final String SPI_NAME = "jwt-authorization-grant-validator";
17+
18+
@Override
19+
public boolean isInternal() {
20+
return true;
21+
}
22+
23+
@Override
24+
public String getName() {
25+
return SPI_NAME;
26+
}
27+
28+
@Override
29+
public Class<? extends Provider> getProviderClass() {
30+
return JWTAuthorizationGrantValidator.class;
31+
}
32+
33+
@Override
34+
public Class<? extends ProviderFactory> getProviderFactoryClass() {
35+
return JWTAuthorizationGrantValidatorFactory.class;
36+
}
37+
38+
}

server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,4 @@ org.keycloak.models.workflow.WorkflowSpi
113113
org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessorSpi
114114
org.keycloak.cache.AlternativeLookupSPI
115115
org.keycloak.cache.LocalCacheSPI
116+
org.keycloak.protocol.oidc.JWTAuthorizationGrantValidatorSpi

services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractBaseJWTValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public abstract class AbstractBaseJWTValidator {
3737

3838
private static final Logger logger = Logger.getLogger(AbstractBaseJWTValidator.class);
3939

40-
protected final ClientAssertionState clientAssertionState;
40+
protected ClientAssertionState clientAssertionState;
4141
protected final KeycloakSession session;
4242
protected final int currentTime;
4343

services/src/main/java/org/keycloak/protocol/oidc/grants/JWTAuthorizationGrantType.java renamed to services/src/main/java/org/keycloak/protocol/oidc/grants/jwtauthorization/JWTAuthorizationGrantType.java

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,36 @@
1515
* limitations under the License.
1616
*/
1717

18-
package org.keycloak.protocol.oidc.grants;
18+
package org.keycloak.protocol.oidc.grants.jwtauthorization;
1919

2020
import jakarta.ws.rs.core.Response;
2121

2222
import org.keycloak.OAuth2Constants;
2323
import org.keycloak.OAuthErrorException;
24+
import org.keycloak.authentication.authenticators.client.ClientAssertionState;
2425
import org.keycloak.broker.provider.BrokeredIdentityContext;
2526
import org.keycloak.broker.provider.JWTAuthorizationGrantProvider;
2627
import org.keycloak.cache.AlternativeLookupProvider;
2728
import org.keycloak.events.Details;
2829
import org.keycloak.events.Errors;
2930
import org.keycloak.events.EventType;
31+
import org.keycloak.jose.jws.JWSInput;
32+
import org.keycloak.jose.jws.JWSInputException;
3033
import org.keycloak.models.ClientModel;
3134
import org.keycloak.models.ClientSessionContext;
3235
import org.keycloak.models.FederatedIdentityModel;
3336
import org.keycloak.models.IdentityProviderModel;
37+
import org.keycloak.models.KeycloakSession;
3438
import org.keycloak.models.UserModel;
3539
import org.keycloak.models.UserSessionModel;
40+
import org.keycloak.protocol.oidc.JWTAuthorizationGrantValidator;
3641
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
3742
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
3843
import org.keycloak.protocol.oidc.TokenManager;
44+
import org.keycloak.protocol.oidc.grants.OAuth2GrantTypeBase;
45+
import org.keycloak.protocol.oidc.grants.jwtauthorization.validator.DefaultJWTAuthorizationGrantValidator;
46+
import org.keycloak.protocol.oidc.grants.jwtauthorization.validator.JWTAuthorizationGrantValidatorBase;
47+
import org.keycloak.representations.JsonWebToken;
3948
import org.keycloak.services.CorsErrorResponseException;
4049
import org.keycloak.services.Urls;
4150
import org.keycloak.services.clientpolicy.ClientPolicyException;
@@ -56,8 +65,38 @@ public Response process(Context context) {
5665

5766
try {
5867

59-
JWTAuthorizationGrantValidator authorizationGrantContext = JWTAuthorizationGrantValidator.createValidator(
68+
if (assertion == null) {
69+
throw new IllegalArgumentException("Missing parameter:" + OAuth2Constants.ASSERTION);
70+
}
71+
72+
JWSInput jws;
73+
try {
74+
jws = new JWSInput(assertion);
75+
} catch (JWSInputException e) {
76+
throw new RuntimeException("The provided assertion is not a valid JWT");
77+
}
78+
79+
String jwtTokenType = jws.getHeader().getType();
80+
JWTAuthorizationGrantValidatorBase authorizationGrantContext =
81+
(JWTAuthorizationGrantValidatorBase) session.getProvider(JWTAuthorizationGrantValidator.class, jwtTokenType);
82+
if( authorizationGrantContext == null){
83+
authorizationGrantContext = createDefaultValidator(
6084
context.getSession(), client, assertion, formParams.getFirst(OAuth2Constants.SCOPE));
85+
} else {
86+
JsonWebToken jwt;
87+
try {
88+
jwt = jws.readJsonContent(JsonWebToken.class);
89+
} catch (JWSInputException e) {
90+
throw new RuntimeException("The provided assertion is not a valid JWT");
91+
}
92+
93+
ClientAssertionState clientAssertionState = new ClientAssertionState(OAuth2Constants.JWT_AUTHORIZATION_GRANT, assertion, jws, jwt);
94+
clientAssertionState.setClient(client);
95+
96+
authorizationGrantContext.setClientAssertionState(clientAssertionState);
97+
authorizationGrantContext.setScopeParam(formParams.getFirst(OAuth2Constants.SCOPE));
98+
}
99+
61100
event.detail(Details.IDENTITY_PROVIDER_ISSUER, authorizationGrantContext.getIssuer());
62101
event.detail(Details.IDENTITY_PROVIDER_USER_ID, authorizationGrantContext.getSubject());
63102

@@ -166,6 +205,10 @@ protected AuthenticationSessionModel createSessionModel(RootAuthenticationSessio
166205
return authSession;
167206
}
168207

208+
protected JWTAuthorizationGrantValidatorBase createDefaultValidator(KeycloakSession session, ClientModel client, String assertion, String scope) {
209+
return DefaultJWTAuthorizationGrantValidator.createValidator(session, client, assertion, scope);
210+
}
211+
169212
@Override
170213
protected boolean useRefreshToken() {
171214
return false; // jwt auth grant never generates the refresh token

services/src/main/java/org/keycloak/protocol/oidc/grants/JWTAuthorizationGrantTypeFactory.java renamed to services/src/main/java/org/keycloak/protocol/oidc/grants/jwtauthorization/JWTAuthorizationGrantTypeFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@
1515
* limitations under the License.
1616
*/
1717

18-
package org.keycloak.protocol.oidc.grants;
18+
package org.keycloak.protocol.oidc.grants.jwtauthorization;
1919

2020

2121
import org.keycloak.Config;
2222
import org.keycloak.OAuth2Constants;
2323
import org.keycloak.common.Profile;
2424
import org.keycloak.models.KeycloakSession;
2525
import org.keycloak.models.KeycloakSessionFactory;
26+
import org.keycloak.protocol.oidc.grants.OAuth2GrantType;
27+
import org.keycloak.protocol.oidc.grants.OAuth2GrantTypeFactory;
2628
import org.keycloak.provider.EnvironmentDependentProviderFactory;
2729

2830
public class JWTAuthorizationGrantTypeFactory implements OAuth2GrantTypeFactory, EnvironmentDependentProviderFactory {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.keycloak.protocol.oidc.grants.jwtauthorization.validator;
2+
3+
import org.keycloak.OAuth2Constants;
4+
import org.keycloak.authentication.authenticators.client.ClientAssertionState;
5+
import org.keycloak.jose.jws.JWSInput;
6+
import org.keycloak.jose.jws.JWSInputException;
7+
import org.keycloak.models.ClientModel;
8+
import org.keycloak.models.KeycloakSession;
9+
import org.keycloak.representations.JsonWebToken;
10+
11+
public class DefaultJWTAuthorizationGrantValidator extends JWTAuthorizationGrantValidatorBase {
12+
13+
public static DefaultJWTAuthorizationGrantValidator createValidator(KeycloakSession session, ClientModel client, String assertion, String scope) {
14+
if (assertion == null) {
15+
throw new IllegalArgumentException("Missing parameter:" + OAuth2Constants.ASSERTION);
16+
}
17+
try {
18+
JWSInput jws = new JWSInput(assertion);
19+
JsonWebToken jwt = jws.readJsonContent(JsonWebToken.class);
20+
ClientAssertionState clientAssertionState = new ClientAssertionState(OAuth2Constants.JWT_AUTHORIZATION_GRANT, assertion, jws, jwt);
21+
clientAssertionState.setClient(client);
22+
return new DefaultJWTAuthorizationGrantValidator(session, scope, clientAssertionState);
23+
} catch (JWSInputException e) {
24+
throw new RuntimeException("The provided assertion is not a valid JWT");
25+
}
26+
}
27+
28+
private DefaultJWTAuthorizationGrantValidator(KeycloakSession session, String scope, ClientAssertionState clientAssertionState) {
29+
super(session, clientAssertionState);
30+
this.scope = scope;
31+
this.audienceAlreadyValidated = false;
32+
}
33+
34+
}

0 commit comments

Comments
 (0)