diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java index 9d545ad0e0f..374298515c7 100644 --- a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java @@ -17,15 +17,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.primitives.Bytes; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.yahoo.athenz.auth.*; import com.yahoo.athenz.auth.impl.CertificateAuthority; import com.yahoo.athenz.auth.impl.SimplePrincipal; -import com.yahoo.athenz.auth.token.AccessToken; -import com.yahoo.athenz.auth.token.IdToken; -import com.yahoo.athenz.auth.token.PrincipalToken; -import com.yahoo.athenz.auth.token.ZTSAccessToken; +import com.yahoo.athenz.auth.token.*; import com.yahoo.athenz.auth.token.jwts.JwtsHelper; import com.yahoo.athenz.auth.token.jwts.JwtsResolver; import com.yahoo.athenz.auth.util.AthenzUtils; @@ -82,6 +77,7 @@ import com.yahoo.athenz.zts.token.AccessTokenRequest; import com.yahoo.athenz.zts.token.AccessTokenScope; import com.yahoo.athenz.zts.token.IdTokenScope; +import com.yahoo.athenz.zts.token.TokenConfigOptions; import com.yahoo.athenz.zts.transportrules.TransportRulesProcessor; import com.yahoo.athenz.zts.utils.ZTSUtils; import com.yahoo.rdl.*; @@ -197,8 +193,7 @@ public class ZTSImpl implements ZTSHandler { protected SecretKey serviceCredsEncryptionKey = null; protected String serviceCredsEncryptionAlgorithm = null; protected boolean jwtCurveRfcSupportOnly = false; - protected ConfigurableJWTProcessor jwtIDTProcessor = null; - protected ConfigurableJWTProcessor jwtJAGProcessor = null; + protected TokenConfigOptions tokenConfigOptions = null; private static final String TYPE_DOMAIN_NAME = "DomainName"; private static final String TYPE_SIMPLE_NAME = "SimpleName"; @@ -407,9 +402,28 @@ public ZTSImpl(CloudStore implCloudStore, DataStore implDataStore) { spiffeUriManager = new SpiffeUriManager(); - // create our jwt process for JAG tokens + // create our jwt process objects and the config for validating + // access token requests - loadJWTProcessors(); + generateTokenConfigOptions(); + } + + void generateTokenConfigOptions() { + + List jwtsResolvers = generateSupportedJAGIssuers(); + + // we're always going to add our own zts server as the last entry + // in case our config files are not updated thus we need to extract + // the public keys from ourselves directly + jwtsResolvers.add(new JwtsResolver(ztsOpenIDIssuer + "/oauth2/keys?rfc=true", null, null)); + + // create our token config options + + tokenConfigOptions = new TokenConfigOptions(); + tokenConfigOptions.setPublicKeyProvider(dataStore); + tokenConfigOptions.setOauth2Issuer(ztsOAuthIssuer); + tokenConfigOptions.setJwtIDTProcessor(JwtsHelper.getJWTProcessor(jwtsResolvers, JwtsHelper.JWT_TYPE_VERIFIER)); + tokenConfigOptions.setJwtJAGProcessor(JwtsHelper.getJWTProcessor(jwtsResolvers, JwtsHelper.JWT_JAG_TYPE_VERIFIER)); } void loadJsonMapper() { @@ -492,16 +506,6 @@ private void setupMetaConfigObjects() { oauthConfig.setToken_endpoint_auth_signing_alg_values_supported(getSupportedSigningAlgValues()); } - protected void loadJWTProcessors() { - List jwtsResolvers = generateSupportedJAGIssuers(); - // we're always going to add our own zts server as the last entry - // in case our config files are not updated thus we need to extract - // the public keys from ourselves directly - jwtsResolvers.add(new JwtsResolver(ztsOpenIDIssuer + "/oauth2/keys?rfc=true", null, null)); - jwtJAGProcessor = JwtsHelper.getJWTProcessor(jwtsResolvers, JwtsHelper.JWT_JAG_TYPE_VERIFIER); - jwtIDTProcessor = JwtsHelper.getJWTProcessor(jwtsResolvers, JwtsHelper.JWT_TYPE_VERIFIER); - } - List generateSupportedJAGIssuers() { // extract jag issuers if configured, the format is a comma separated @@ -2608,7 +2612,7 @@ public AccessTokenResponse postAccessTokenRequest(ResourceContext ctx, String re AccessTokenRequest accessTokenRequest; try { - accessTokenRequest = new AccessTokenRequest(request, dataStore, ztsOAuthIssuer); + accessTokenRequest = new AccessTokenRequest(request, tokenConfigOptions); if (principal == null) { principal = accessTokenRequest.getPrincipal(); ((RsrcCtxWrapper) ctx).setPrincipal(principal); @@ -2639,12 +2643,19 @@ public AccessTokenResponse postAccessTokenRequest(ResourceContext ctx, String re case JAG_JWT_BEARER: return processAccessTokenJAGRequest(ctx, accessTokenRequest, principal.getFullName(), principalDomain, caller); + case TOKEN_EXCHANGE: + return processAccessTokenExchangeRequest(ctx, principal, accessTokenRequest, principalDomain, caller); case ACCESS_TOKEN: default: return processAccessTokenStandardRequest(ctx, principal, accessTokenRequest, principalDomain, caller); } } + AccessTokenResponse processAccessTokenExchangeRequest(ResourceContext ctx, Principal principal, + AccessTokenRequest accessTokenRequest, final String principalDomain, final String caller) { + throw requestError("Not Yet implemented", caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain); + } + AccessTokenResponse processAccessTokenJAGExchange(ResourceContext ctx, Principal principal, AccessTokenRequest accessTokenRequest, final String principalDomain, final String caller) { @@ -2652,15 +2663,11 @@ AccessTokenResponse processAccessTokenJAGExchange(ResourceContext ctx, Principal String principalName = principal.getFullName(); - // we need to validate our subject + // our subject token is required for jag token exchange + // and has been validated already during the request object + // creation - IdToken subjectToken; - try { - subjectToken = new IdToken(accessTokenRequest.getSubjectToken(), jwtIDTProcessor); - } catch (Exception ex) { - LOGGER.error("Unable to parse subject token: {}", ex.getMessage()); - throw requestError("Invalid subject token", caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain); - } + OAuth2Token subjectToken = accessTokenRequest.getSubjectTokenObj(); // the audience of the subject token must be our client id which // in our case is the principal name @@ -2762,17 +2769,10 @@ AccessTokenResponse processAccessTokenJAGExchange(ResourceContext ctx, Principal AccessTokenResponse processAccessTokenJAGRequest(ResourceContext ctx, AccessTokenRequest accessTokenRequest, final String clientPrincipalName, final String clientPrincipalDomain, final String caller) { - // first we need to validate the jag assertion with our processor - // which will validate that our token is properly signed and - // typed as oauth-id-jag+jwt + // our jag token is required for jag token requests and has been validated + // already during the request object creation - AccessToken jagToken; - try { - jagToken = new AccessToken(accessTokenRequest.getAssertion(), jwtJAGProcessor); - } catch (Exception ex) { - LOGGER.error("Unable to parse jag assertion: {}", ex.getMessage()); - throw requestError("Invalid jag assertion", caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, clientPrincipalDomain); - } + AccessToken jagToken = accessTokenRequest.getJagTokenObj(); // next we need to validate that the aud claim MUST match // our server oidc/oauth issuer value diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenRequest.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenRequest.java index 0626e9dbee8..13e8289fbc5 100644 --- a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenRequest.java +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenRequest.java @@ -15,9 +15,9 @@ */ package com.yahoo.athenz.zts.token; -import com.yahoo.athenz.auth.KeyStore; import com.yahoo.athenz.auth.Principal; import com.yahoo.athenz.auth.impl.SimplePrincipal; +import com.yahoo.athenz.auth.token.AccessToken; import com.yahoo.athenz.auth.token.OAuth2Token; import com.yahoo.athenz.zts.ZTSConsts; import com.yahoo.athenz.zts.utils.ZTSUtils; @@ -42,6 +42,7 @@ public class AccessTokenRequest { public enum RequestType { ACCESS_TOKEN, + TOKEN_EXCHANGE, JAG_TOKEN_EXCHANGE, JAG_JWT_BEARER } @@ -68,6 +69,8 @@ public enum RequestType { private static final String OAUTH_TOKEN_TYPE_JAG = "urn:ietf:params:oauth:token-type:id-jag"; private static final String OAUTH_TOKEN_TYPE_ID = "urn:ietf:params:oauth:token-type:id_token"; + private static final String OAUTH_TOKEN_TYPE_ACCESS = "urn:ietf:params:oauth:token-type:access_token"; + private static final String OAUTH_TOKEN_TYPE_JWT = "urn:ietf:params:oauth:token-type:jwt"; private static final String OAUTH_GRANT_CLIENT_CREDENTIALS = "client_credentials"; private static final String OAUTH_GRANT_TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange"; @@ -92,8 +95,11 @@ public enum RequestType { int expiryTime = 0; boolean useOpenIDIssuer = false; RequestType requestType; + OAuth2Token actorTokenObj = null; + OAuth2Token subjectTokenObj = null; + AccessToken jagTokenObj = null; - public AccessTokenRequest(final String body, KeyStore publicKeyProvider, final String oauth2Issuer) { + public AccessTokenRequest(final String body, TokenConfigOptions options) { String[] comps = body.split("&"); for (String comp : comps) { @@ -183,17 +189,28 @@ public AccessTokenRequest(final String body, KeyStore publicKeyProvider, final S // RFC 6749 access token request requestType = RequestType.ACCESS_TOKEN; - validateAccessTokenRequest(publicKeyProvider, oauth2Issuer); + validateAccessTokenRequest(options); break; case OAUTH_GRANT_TOKEN_EXCHANGE: + // Supported token exchange types: standard and jag + // OAuth 2.0 Token Exchange + // https://www.rfc-editor.org/rfc/rfc8693.html // Identity Assertion Authorization Grant // https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/ - requestType = RequestType.JAG_TOKEN_EXCHANGE; - validateTokenExchangeRequest(publicKeyProvider, oauth2Issuer); + if (OAUTH_TOKEN_TYPE_JAG.equals(requestedTokenType)) { + requestType = RequestType.JAG_TOKEN_EXCHANGE; + validateJAGTokenExchangeRequest(options); + } else if (OAUTH_TOKEN_TYPE_ACCESS.equals(requestedTokenType) || StringUtil.isEmpty(requestedTokenType)) { + requestType = RequestType.TOKEN_EXCHANGE; + validateAccessTokenExchangeRequest(options); + } else { + throw new IllegalArgumentException("Invalid requested token type: " + requestedTokenType); + } + break; case OAUTH_GRANT_JWT_BEARER: @@ -202,7 +219,7 @@ public AccessTokenRequest(final String body, KeyStore publicKeyProvider, final S // https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/ requestType = RequestType.JAG_JWT_BEARER; - validateJWTBearerRequest(); + validateJWTBearerRequest(options); break; default: @@ -210,7 +227,7 @@ public AccessTokenRequest(final String body, KeyStore publicKeyProvider, final S } } - void validateAccessTokenRequest(KeyStore publicKeyProvider, final String oauth2Issuer) { + void validateAccessTokenRequest(TokenConfigOptions options) { // even though scope is optional in RFC 6749, because we're a multi-tenant // service and we have no other way of identifying what access the client @@ -224,16 +241,10 @@ void validateAccessTokenRequest(KeyStore publicKeyProvider, final String oauth2I // have a client assertion type as well, so let's validate // our specified token and generate a principal object - validateClientAssertion(publicKeyProvider, oauth2Issuer); + validateClientAssertion(options); } - void validateTokenExchangeRequest(KeyStore publicKeyProvider, final String oauth2Issuer) { - - // we must have a requested token type - - if (!OAUTH_TOKEN_TYPE_JAG.equals(requestedTokenType)) { - throw new IllegalArgumentException("Invalid requested token type: " + requestedTokenType); - } + void validateJAGTokenExchangeRequest(TokenConfigOptions options) { // even though scope is optional in RFC 6749, because we're a multi-tenant // service and we have no other way of identifying what access the client @@ -254,30 +265,87 @@ void validateTokenExchangeRequest(KeyStore publicKeyProvider, final String oauth // we'll validate accordingly. the actor_token and actor_token_type // are optional and not used in the ID Token Authz Grant spec. - if (StringUtil.isEmpty(subjectToken)) { - throw new IllegalArgumentException("Invalid request: no subject token provided"); - } if (!OAUTH_TOKEN_TYPE_ID.equals(subjectTokenType)) { throw new IllegalArgumentException("Invalid subject token type: " + subjectTokenType); } + validateSubjectToken(options); // if we're provided with a client assertion then we must // have a client assertion type as well, so let's validate // our specified token and generate a principal object - validateClientAssertion(publicKeyProvider, oauth2Issuer); + validateClientAssertion(options); + } + + void validateAccessTokenExchangeRequest(TokenConfigOptions options) { + + // for token exchange requests we must have subject token and type. + // we currently only support id and access tokens as subject tokens. + + if (!validJwtTokenType(subjectTokenType)) { + throw new IllegalArgumentException("Invalid subject token type: " + subjectTokenType); + } + validateSubjectToken(options); + + // we must have audience specified + + if (StringUtil.isEmpty(audience)) { + throw new IllegalArgumentException("Invalid request: no audience provided"); + } + + // if we have an actor token specified then we must have + // an actor token type as well. So let's validate accordingly. + + if (!StringUtil.isEmpty(actorToken)) { + if (!validJwtTokenType(actorTokenType)) { + throw new IllegalArgumentException("Invalid actor token type: " + actorTokenType); + } + + try { + actorTokenObj = new OAuth2Token(actorToken, options.getPublicKeyProvider(), + options.getOauth2Issuer()); + } catch (Exception ex) { + throw new IllegalArgumentException("Invalid actor token: " + ex.getMessage()); + } + } } - void validateJWTBearerRequest() { + boolean validJwtTokenType(final String tokenType) { + return (OAUTH_TOKEN_TYPE_ID.equals(tokenType) || OAUTH_TOKEN_TYPE_ACCESS.equals(tokenType) + || OAUTH_TOKEN_TYPE_JWT.equals(tokenType)); + } + + void validateJWTBearerRequest(TokenConfigOptions options) { // the only required attribute is assertion if (StringUtil.isEmpty(assertion)) { throw new IllegalArgumentException("Invalid request: no assertion provided"); } + + // we need to validate the jag assertion with our processor + // which will validate that our token is properly signed and + // typed as oauth-id-jag+jwt + + try { + jagTokenObj = new AccessToken(assertion, options.getJwtJAGProcessor()); + } catch (Exception ex) { + throw new IllegalArgumentException("Invalid assertion token: " + ex.getMessage()); + } } - void validateClientAssertion(KeyStore publicKeyProvider, final String oauth2Issuer) { + void validateSubjectToken(TokenConfigOptions options) { + if (StringUtil.isEmpty(subjectToken)) { + throw new IllegalArgumentException("Invalid request: no subject token provided"); + } + try { + subjectTokenObj = new OAuth2Token(subjectToken, options.getJwtIDTProcessor()); + } catch (Exception ex) { + throw new IllegalArgumentException("Invalid subject token: " + ex.getMessage()); + } + } + + void validateClientAssertion(TokenConfigOptions options) { if (!StringUtil.isEmpty(clientAssertion)) { @@ -291,7 +359,8 @@ void validateClientAssertion(KeyStore publicKeyProvider, final String oauth2Issu // token provided and, if yes, generate our principal object try { - OAuth2Token token = new OAuth2Token(clientAssertion, publicKeyProvider, oauth2Issuer); + OAuth2Token token = new OAuth2Token(clientAssertion, options.getPublicKeyProvider(), + options.getOauth2Issuer()); principal = SimplePrincipal.create(token.getClientIdDomainName(), token.getClientIdServiceName(), clientAssertion, token.getIssueTime(), null); } catch (Exception ex) { @@ -376,6 +445,18 @@ public Principal getPrincipal() { return principal; } + public OAuth2Token getActorTokenObj() { + return actorTokenObj; + } + + public OAuth2Token getSubjectTokenObj() { + return subjectTokenObj; + } + + public AccessToken getJagTokenObj() { + return jagTokenObj; + } + public String getQueryLogData() { StringBuilder stringBuilder = new StringBuilder(); diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/TokenConfigOptions.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/TokenConfigOptions.java new file mode 100644 index 00000000000..afc63ba93a8 --- /dev/null +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/TokenConfigOptions.java @@ -0,0 +1,60 @@ +/* + * Copyright The Athenz Authors + * + * 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 com.yahoo.athenz.zts.token; + +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.yahoo.athenz.auth.KeyStore; + +public class TokenConfigOptions { + + KeyStore publicKeyProvider = null; + String oauth2Issuer = null; + ConfigurableJWTProcessor jwtIDTProcessor = null; + ConfigurableJWTProcessor jwtJAGProcessor = null; + + public KeyStore getPublicKeyProvider() { + return publicKeyProvider; + } + + public void setPublicKeyProvider(KeyStore publicKeyProvider) { + this.publicKeyProvider = publicKeyProvider; + } + + public String getOauth2Issuer() { + return oauth2Issuer; + } + + public void setOauth2Issuer(String oauth2Issuer) { + this.oauth2Issuer = oauth2Issuer; + } + + public ConfigurableJWTProcessor getJwtIDTProcessor() { + return jwtIDTProcessor; + } + + public void setJwtIDTProcessor(ConfigurableJWTProcessor jwtIDTProcessor) { + this.jwtIDTProcessor = jwtIDTProcessor; + } + + public ConfigurableJWTProcessor getJwtJAGProcessor() { + return jwtJAGProcessor; + } + + public void setJwtJAGProcessor(ConfigurableJWTProcessor jwtJAGProcessor) { + this.jwtJAGProcessor = jwtJAGProcessor; + } +} diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplAccessTokenTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplAccessTokenTest.java index de1ead699a3..e8481f70507 100644 --- a/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplAccessTokenTest.java +++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplAccessTokenTest.java @@ -43,6 +43,7 @@ import com.yahoo.athenz.zts.store.MockZMSFileChangeLogStore; import com.yahoo.athenz.zts.token.AccessTokenRequest; import com.yahoo.athenz.zts.token.AccessTokenScope; +import com.yahoo.athenz.zts.token.TokenConfigOptions; import com.yahoo.rdl.Struct; import com.yahoo.rdl.Timestamp; import jakarta.servlet.http.HttpServletRequest; @@ -115,7 +116,7 @@ public void setupClass() { System.setProperty(ZTSConsts.ZTS_PROP_CERT_ALLOWED_O_VALUES, "Athenz, Inc.|My Test Company|Athenz|Yahoo"); System.setProperty(ZTSConsts.ZTS_PROP_NOAUTH_URI_LIST, "/zts/v1/schema,/zts/v1/status"); System.setProperty(ZTSConsts.ZTS_PROP_VALIDATE_SERVICE_SKIP_DOMAINS, "screwdriver,rbac.*"); - System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://athenz.cloud:4443/zts/v1"); + System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://athenz.io:4443/zts/v1"); // setup our metric class @@ -160,7 +161,7 @@ public void setup() { ZTSTestUtils.deleteDirectory(new File("/tmp/zts_server_cert_store")); System.setProperty(ZTSConsts.ZTS_PROP_CERT_FILE_STORE_PATH, "/tmp/zts_server_cert_store"); System.setProperty(ZTSConsts.ZTS_PROP_VALIDATE_SERVICE_IDENTITY, "false"); - System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://athenz.cloud:4443/zts/v1"); + System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://athenz.io:4443/zts/v1"); // enable ip validation for cert requests @@ -781,10 +782,10 @@ public void testPostAccessTokenRequestOpenIdScope() throws JOSEException { @Test public void testPostAccessTokenRequestOpenIdScopeOpenIDIssuer() throws JOSEException { - System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://openid.athenz.cloud:4443/zts/v1"); - System.setProperty(ZTSConsts.ZTS_PROP_OAUTH_ISSUER, "https://oauth.athenz.cloud:4443/zts/v1"); + System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://openid.athenz.io:4443/zts/v1"); + System.setProperty(ZTSConsts.ZTS_PROP_OAUTH_ISSUER, "https://oauth.athenz.io:4443/zts/v1"); - testPostAccessTokenRequestOpenIdScope("https://openid.athenz.cloud:4443/zts/v1", "&openid_issuer=true"); + testPostAccessTokenRequestOpenIdScope("https://openid.athenz.io:4443/zts/v1", "&openid_issuer=true"); System.clearProperty(ZTSConsts.ZTS_PROP_OAUTH_ISSUER); System.clearProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER); @@ -793,10 +794,10 @@ public void testPostAccessTokenRequestOpenIdScopeOpenIDIssuer() throws JOSEExcep @Test public void testPostAccessTokenRequestOpenIdScopeOAuthIssuer() throws JOSEException { - System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://openid.athenz.cloud:4443/zts/v1"); - System.setProperty(ZTSConsts.ZTS_PROP_OAUTH_ISSUER, "https://oauth.athenz.cloud:4443/zts/v1"); + System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://openid.athenz.io:4443/zts/v1"); + System.setProperty(ZTSConsts.ZTS_PROP_OAUTH_ISSUER, "https://oauth.athenz.io:4443/zts/v1"); - testPostAccessTokenRequestOpenIdScope("https://oauth.athenz.cloud:4443/zts/v1", "&openid_issuer=false"); + testPostAccessTokenRequestOpenIdScope("https://oauth.athenz.io:4443/zts/v1", "&openid_issuer=false"); System.clearProperty(ZTSConsts.ZTS_PROP_OAUTH_ISSUER); System.clearProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER); @@ -1852,7 +1853,7 @@ public void testProcessAccessTokenJAGRequestSuccess() throws JOSEException { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -1903,12 +1904,13 @@ public void testProcessAccessTokenJAGRequestSuccess() throws JOSEException { public void testProcessAccessTokenJAGRequestSuccessWithOpenIDIssuer() throws JOSEException { System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem"); - System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://openid.athenz.cloud:4443/zts/v1"); - System.setProperty(ZTSConsts.ZTS_PROP_OAUTH_ISSUER, "https://oauth.athenz.cloud:4443/zts/v1"); + System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://openid.athenz.io:4443/zts/v1"); + System.setProperty(ZTSConsts.ZTS_PROP_OAUTH_ISSUER, "https://oauth.athenz.io:4443/zts/v1"); CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); + System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -1919,7 +1921,7 @@ public void testProcessAccessTokenJAGRequestSuccessWithOpenIDIssuer() throws JOS PrivateKey privateKey = Crypto.loadPrivateKey(privateKeyFile); long expiryTime = System.currentTimeMillis() / 1000 + 3600; String jagToken = createJagToken(privateKey, "0", "user_domain.user", "coretech.jwt", - "coretech:domain", "https://openid.athenz.cloud:4443/zts/v1", expiryTime); + "coretech:domain", "https://openid.athenz.io:4443/zts/v1", expiryTime); HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class); Mockito.when(servletRequest.isSecure()).thenReturn(true); @@ -1943,7 +1945,7 @@ public void testProcessAccessTokenJAGRequestSuccessWithOpenIDIssuer() throws JOS assertTrue(signedJWT.verify(verifier)); JWTClaimsSet claimSet = signedJWT.getJWTClaimsSet(); - assertEquals(claimSet.getIssuer(), "https://openid.athenz.cloud:4443/zts/v1"); + assertEquals(claimSet.getIssuer(), "https://openid.athenz.io:4443/zts/v1"); } catch (ParseException ex) { fail(ex.getMessage()); } @@ -1959,7 +1961,7 @@ public void testProcessAccessTokenJAGRequestSuccessWithSpecificRoles() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -1995,7 +1997,7 @@ public void testProcessAccessTokenJAGRequestInvalidAssertion() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -2018,7 +2020,7 @@ public void testProcessAccessTokenJAGRequestInvalidAssertion() { fail(); } catch (ResourceException ex) { assertEquals(ex.getCode(), 400); - assertTrue(ex.getMessage().contains("Invalid jag assertion")); + assertTrue(ex.getMessage().contains("Invalid assertion token")); } } @@ -2029,7 +2031,7 @@ public void testProcessAccessTokenJAGRequestInvalidAudience() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -2067,7 +2069,7 @@ public void testProcessAccessTokenJAGRequestInvalidClientId() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -2105,7 +2107,7 @@ public void testProcessAccessTokenJAGRequestMissingScope() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -2143,7 +2145,7 @@ public void testProcessAccessTokenJAGRequestMissingSubject() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -2181,7 +2183,7 @@ public void testProcessAccessTokenJAGRequestInvalidDomain() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -2218,7 +2220,7 @@ public void testProcessAccessTokenJAGRequestDomainNotFound() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -2257,7 +2259,7 @@ public void testProcessAccessTokenJAGRequestNoAccessibleRoles() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -2294,7 +2296,7 @@ public void testProcessAccessTokenJAGRequestMultipleRolesScopeResponse() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -2332,7 +2334,7 @@ public void testProcessAccessTokenJAGRequestRoleMismatch() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtJAGProcessor = createJAGProcessor(); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); @@ -2393,7 +2395,7 @@ private String createIdToken(PrivateKey privateKey, String keyId, String subject .subject(subject) .issueTime(Date.from(Instant.ofEpochSecond(now))) .expirationTime(Date.from(Instant.ofEpochSecond(expiryTime))) - .issuer("https://athenz.cloud:4443/zts/v1") + .issuer("https://athenz.io:4443/zts/v1") .audience(audience) .claim("ver", 1) .claim("auth_time", now) @@ -2428,7 +2430,7 @@ public void testProcessAccessTokenJAGExchangeSuccess() throws JOSEException { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); // set back to our zts rsa private key System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2506,7 +2508,8 @@ public void testProcessAccessTokenJAGExchangeScopeMissingRoles() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); + ztsImpl.tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); // set back to our zts rsa private key System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2556,7 +2559,7 @@ public void testProcessAccessTokenJAGExchangeNotAuthorized() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); // set back to our zts rsa private key System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2613,7 +2616,7 @@ public void testProcessAccessTokenJAGExchangeInvalidSubjectToken() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2627,19 +2630,20 @@ public void testProcessAccessTokenJAGExchangeInvalidSubjectToken() { // Create request with invalid subject token try { + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); + AccessTokenRequest accessTokenRequest = new AccessTokenRequest( "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" + "&subject_token=invalid.jwt.token&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=coretech:role.writers", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); fail("Expected ResourceException for invalid subject token"); - } catch (ResourceException ex) { - assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + } catch (IllegalArgumentException ex) { assertTrue(ex.getMessage().contains("Invalid subject token")); } @@ -2652,7 +2656,7 @@ public void testProcessAccessTokenJAGExchangeWrongAudience() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2673,13 +2677,14 @@ public void testProcessAccessTokenJAGExchangeWrongAudience() { ResourceContext context = createResourceContext(principal); try { + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); AccessTokenRequest accessTokenRequest = new AccessTokenRequest( "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=coretech:role.writers", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); @@ -2698,7 +2703,7 @@ public void testProcessAccessTokenJAGExchangeEmptyScope() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2716,6 +2721,7 @@ public void testProcessAccessTokenJAGExchangeEmptyScope() { "v=U1;d=user_domain;n=proxy-user1;s=signature", 0, null); assertNotNull(principal); ResourceContext context = createResourceContext(principal); + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); try { new AccessTokenRequest( @@ -2723,7 +2729,7 @@ public void testProcessAccessTokenJAGExchangeEmptyScope() { + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); fail("Expected IllegalArgumentException for empty scope"); } catch (IllegalArgumentException ex) { assertTrue(ex.getMessage().contains("Invalid request: no scope provided")); @@ -2736,7 +2742,7 @@ public void testProcessAccessTokenJAGExchangeEmptyScope() { + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=openid", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); @@ -2749,13 +2755,22 @@ public void testProcessAccessTokenJAGExchangeEmptyScope() { cloudStore.close(); } + private TokenConfigOptions createTokenConfigOptions(ZTSImpl ztsImpl) { + TokenConfigOptions tokenConfigOptions = new TokenConfigOptions(); + tokenConfigOptions.setPublicKeyProvider(null); + tokenConfigOptions.setOauth2Issuer(ztsImpl.ztsOAuthIssuer); + tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); + tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); + return tokenConfigOptions; + } + @Test public void testProcessAccessTokenJAGExchangeDomainNotFound() { System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem"); CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2770,6 +2785,7 @@ public void testProcessAccessTokenJAGExchangeDomainNotFound() { "v=U1;d=user_domain;n=proxy-user1;s=signature", 0, null); assertNotNull(principal); ResourceContext context = createResourceContext(principal); + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); try { AccessTokenRequest accessTokenRequest = new AccessTokenRequest( @@ -2778,7 +2794,7 @@ public void testProcessAccessTokenJAGExchangeDomainNotFound() { + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=nonexistent:role.writers", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); @@ -2797,7 +2813,7 @@ public void testProcessAccessTokenJAGExchangeSubjectNoAccess() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2818,6 +2834,7 @@ public void testProcessAccessTokenJAGExchangeSubjectNoAccess() { "v=U1;d=user_domain;n=proxy-user1;s=signature", 0, null); assertNotNull(principal); ResourceContext context = createResourceContext(principal); + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); try { AccessTokenRequest accessTokenRequest = new AccessTokenRequest( @@ -2826,7 +2843,7 @@ public void testProcessAccessTokenJAGExchangeSubjectNoAccess() { + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=coretech:role.writers", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); @@ -2844,7 +2861,7 @@ public void testProcessAccessTokenJAGExchangePrincipalNotAuthorized() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2864,6 +2881,7 @@ public void testProcessAccessTokenJAGExchangePrincipalNotAuthorized() { "v=U1;d=user_domain;n=proxy-user1;s=signature", 0, null); assertNotNull(principal); ResourceContext context = createResourceContext(principal); + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); try { AccessTokenRequest accessTokenRequest = new AccessTokenRequest( @@ -2872,7 +2890,7 @@ public void testProcessAccessTokenJAGExchangePrincipalNotAuthorized() { + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=coretech:role.writers", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); @@ -2891,7 +2909,7 @@ public void testProcessAccessTokenJAGExchangePartialAccess() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2913,6 +2931,7 @@ public void testProcessAccessTokenJAGExchangePartialAccess() { "v=U1;d=user_domain;n=proxy-user1;s=signature", 0, null); assertNotNull(principal); ResourceContext context = createResourceContext(principal); + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); try { // Request both roles but subject only has access to one @@ -2922,7 +2941,7 @@ public void testProcessAccessTokenJAGExchangePartialAccess() { + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=coretech:role.writers coretech:role.readers", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); @@ -2940,7 +2959,7 @@ public void testProcessAccessTokenJAGExchangeMultipleRoles() throws JOSEExceptio CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -2962,6 +2981,7 @@ public void testProcessAccessTokenJAGExchangeMultipleRoles() throws JOSEExceptio "v=U1;d=user_domain;n=proxy-user1;s=signature", 0, null); assertNotNull(principal); ResourceContext context = createResourceContext(principal); + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); AccessTokenRequest accessTokenRequest = new AccessTokenRequest( "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" @@ -2969,7 +2989,7 @@ public void testProcessAccessTokenJAGExchangeMultipleRoles() throws JOSEExceptio + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=coretech:role.writers coretech:role.readers", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); AccessTokenResponse response = ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); @@ -3005,7 +3025,7 @@ public void testProcessAccessTokenJAGExchangeWithExpiryTime() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -3025,6 +3045,7 @@ public void testProcessAccessTokenJAGExchangeWithExpiryTime() { "v=U1;d=user_domain;n=proxy-user1;s=signature", 0, null); assertNotNull(principal); ResourceContext context = createResourceContext(principal); + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); // Request with specific expiry time AccessTokenRequest accessTokenRequest = new AccessTokenRequest( @@ -3034,7 +3055,7 @@ public void testProcessAccessTokenJAGExchangeWithExpiryTime() { + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=coretech:role.writers" + "&expires_in=600", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); AccessTokenResponse response = ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); @@ -3051,7 +3072,7 @@ public void testProcessAccessTokenJAGExchangeInvalidRoleName() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -3069,6 +3090,7 @@ public void testProcessAccessTokenJAGExchangeInvalidRoleName() { "v=U1;d=user_domain;n=proxy-user1;s=signature", 0, null); assertNotNull(principal); ResourceContext context = createResourceContext(principal); + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); try { // Use invalid role name with special characters @@ -3078,7 +3100,7 @@ public void testProcessAccessTokenJAGExchangeInvalidRoleName() { + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=coretech:role.invalid@role", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); @@ -3096,7 +3118,7 @@ public void testProcessAccessTokenJAGExchangeInvalidDomainName() { CloudStore cloudStore = new CloudStore(); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); - ztsImpl.jwtIDTProcessor = createIDTokenProcessor(); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); @@ -3111,6 +3133,7 @@ public void testProcessAccessTokenJAGExchangeInvalidDomainName() { "v=U1;d=user_domain;n=proxy-user1;s=signature", 0, null); assertNotNull(principal); ResourceContext context = createResourceContext(principal); + TokenConfigOptions tokenConfigOptions = createTokenConfigOptions(ztsImpl); try { // Use invalid domain name @@ -3120,7 +3143,7 @@ public void testProcessAccessTokenJAGExchangeInvalidDomainName() { + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + "&scope=invalid@domain:role.writers", - null, ztsImpl.ztsOAuthIssuer); + tokenConfigOptions); ztsImpl.processAccessTokenJAGExchange(context, principal, accessTokenRequest, "user_domain", "postAccessTokenRequest"); @@ -3215,4 +3238,53 @@ private void addJAGExchangePolicy(String domainName, String principalName, Strin .setSignature(Crypto.sign(SignUtils.asCanonicalString(domainData), privateKey)) .setKeyId("0"), false); } + + @Test + public void testProcessAccessTokenExchangeNotImplemented() { + System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem"); + + CloudStore cloudStore = new CloudStore(); + ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); + ztsImpl.tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); + + // set back to our zts rsa private key + System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); + + // Create domain with roles and policies + SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true); + store.processSignedDomain(signedDomain, false); + + // Add JAG exchange authorization policy + addJAGExchangePolicy("coretech", "user_domain.proxy-user1", "writers"); + + // Load EC private key for creating tokens + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createIdToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime); + + // Create principal for proxy-user1 who will request the token exchange + Principal principal = SimplePrincipal.create("user_domain", "proxy-user1", + "v=U1;d=user_domain;n=proxy-user1;s=signature", 0, null); + assertNotNull(principal); + ResourceContext context = createResourceContext(principal); + + final String tokenRequest = "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&requested_token_type=urn:ietf:params:oauth:token-type:access_token" + + "&subject_token=" + subjectToken + "&audience=https://athenz.io" + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + + "&scope=coretech:role.writers"; + + try { + ztsImpl.postAccessTokenRequest(context, tokenRequest); + fail(); + } catch (Exception ex) { + assertTrue(ex.getMessage().contains("Not Yet implemented")); + } + + cloudStore.close(); + } } diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java index 119fc8cbe48..bf1bcbf96d8 100644 --- a/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java +++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java @@ -14696,14 +14696,14 @@ public void testLoadJWTProcessor() { ZTSImpl testZts = new ZTSImpl(cloudStore, testStore); // Verify that jwtJAGProcessor is not null after initialization - assertNotNull(testZts.jwtJAGProcessor); + assertNotNull(testZts.tokenConfigOptions.getJwtJAGProcessor()); } @Test public void testLoadJWTProcessorVerifyNonNull() { // Test that loadJWTProcessor creates a non-null jwtJAGProcessor // The existing zts instance from setup should have called loadJWTProcessor - assertNotNull(zts.jwtJAGProcessor); + assertNotNull(zts.tokenConfigOptions.getJwtJAGProcessor()); } @Test @@ -14737,6 +14737,6 @@ public void testLoadJWTProcessorAddsZtsIssuer() { // But loadJWTProcessor should add the ZTS server as the last entry // which is verified by the non-null jwtJAGProcessor - assertNotNull(zts.jwtJAGProcessor); + assertNotNull(zts.tokenConfigOptions.getJwtJAGProcessor()); } } diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenRequestTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenRequestTest.java index d50684ea5fa..50111548529 100644 --- a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenRequestTest.java +++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenRequestTest.java @@ -15,17 +15,23 @@ */ package com.yahoo.athenz.zts.token; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.*; import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; import com.yahoo.athenz.auth.KeyStore; import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.token.AccessToken; +import com.yahoo.athenz.auth.token.jwts.JwtsHelper; +import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver; import com.yahoo.athenz.auth.util.Crypto; +import org.eclipse.jetty.util.StringUtil; import org.mockito.Mockito; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.io.File; @@ -33,16 +39,30 @@ import java.security.interfaces.ECPrivateKey; import java.time.Instant; import java.util.Date; +import java.util.Objects; import static org.testng.Assert.*; public class AccessTokenRequestTest { + private final ClassLoader classLoader = this.getClass().getClassLoader(); + + private TokenConfigOptions defaultConfigOptions = null; + + @BeforeMethod + public void setup() { + defaultConfigOptions = new TokenConfigOptions(); + defaultConfigOptions.setPublicKeyProvider(null); + defaultConfigOptions.setOauth2Issuer(null); + defaultConfigOptions.setJwtJAGProcessor(createJAGProcessor()); + defaultConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); + } + @Test public void testAccessTokenRequest() { AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers" - + "&authorization_details=details&expires_in=100&proxy_principal_spiffe_uris=", null, null); + + "&authorization_details=details&expires_in=100&proxy_principal_spiffe_uris=", defaultConfigOptions); assertNotNull(request); assertEquals(request.getGrantType(), "client_credentials"); assertEquals(request.getScope(), "coretech:role.writers"); @@ -56,7 +76,7 @@ public void testAccessTokenRequestInvalidGrant() { try { new AccessTokenRequest("grant_type=unknown&scope=coretech:role.writers" - + "&authorization_details=details", null, null); + + "&authorization_details=details", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid grant request: unknown"); @@ -67,7 +87,7 @@ public void testAccessTokenRequestInvalidGrant() { public void testAccessTokenRequestEmptyScope() { try { - new AccessTokenRequest("grant_type=client_credentials&scope=&expiry_time=100", null, null); + new AccessTokenRequest("grant_type=client_credentials&scope=&expiry_time=100", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no scope provided"); @@ -80,7 +100,7 @@ public void testAccessTokenRequestValidSpiffeUri() { // first valid uri test AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test" - + "&proxy_principal_spiffe_uris=spiffe://data/sa/service", null, null); + + "&proxy_principal_spiffe_uris=spiffe://data/sa/service", defaultConfigOptions); assertNotNull(request); assertEquals(request.getGrantType(), "client_credentials"); assertEquals(request.getScope(), "test"); @@ -90,14 +110,14 @@ public void testAccessTokenRequestValidSpiffeUri() { // uri with leading space request = new AccessTokenRequest("grant_type=client_credentials&scope=test" - + "&proxy_principal_spiffe_uris= spiffe://data/sa/service", null, null); + + "&proxy_principal_spiffe_uris= spiffe://data/sa/service", defaultConfigOptions); assertNotNull(request); assertEquals(request.getProxyPrincipalsSpiffeUris().get(0), "spiffe://data/sa/service"); // uri with multiple values request = new AccessTokenRequest("grant_type=client_credentials&scope=test" - + "&proxy_principal_spiffe_uris=spiffe://data/sa/service,spiffe://sports/sa/api", null, null); + + "&proxy_principal_spiffe_uris=spiffe://data/sa/service,spiffe://sports/sa/api", defaultConfigOptions); assertNotNull(request); assertEquals(request.getProxyPrincipalsSpiffeUris().size(), 2); assertTrue(request.getProxyPrincipalsSpiffeUris().contains("spiffe://data/sa/service")); @@ -106,7 +126,7 @@ public void testAccessTokenRequestValidSpiffeUri() { // uri with spaces around the separator request = new AccessTokenRequest("grant_type=client_credentials&scope=test" - + "&proxy_principal_spiffe_uris=spiffe://data/sa/service , spiffe://sports/sa/api", null, null); + + "&proxy_principal_spiffe_uris=spiffe://data/sa/service , spiffe://sports/sa/api", defaultConfigOptions); assertNotNull(request); assertEquals(request.getProxyPrincipalsSpiffeUris().size(), 2); assertTrue(request.getProxyPrincipalsSpiffeUris().contains("spiffe://data/sa/service")); @@ -116,21 +136,21 @@ public void testAccessTokenRequestValidSpiffeUri() { @Test public void testAccessTokenRequestInvalidSpiffeUri() { try { - new AccessTokenRequest("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=https://athenz.io", null, null); + new AccessTokenRequest("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=https://athenz.io", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid spiffe uri specified: https://athenz.io"); } try { - new AccessTokenRequest("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=spiffe://athenz/sa/service,https://athenz.io", null, null); + new AccessTokenRequest("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=spiffe://athenz/sa/service,https://athenz.io", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid spiffe uri specified: https://athenz.io"); } try { - new AccessTokenRequest("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=spiffe://a .io", null, null); + new AccessTokenRequest("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=spiffe://a .io", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid spiffe uri specified: spiffe://a .io"); @@ -144,7 +164,7 @@ public void testAccessTokenRequestWithClientAssertionFailures() { try { new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers" - + "&authorization_details=details&expires_in=100&client_assertion=jwt", null, null); + + "&authorization_details=details&expires_in=100&client_assertion=jwt", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no client assertion type provided"); @@ -155,7 +175,7 @@ public void testAccessTokenRequestWithClientAssertionFailures() { try { new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers" + "&authorization_details=details&expires_in=100&client_assertion=jwt" - + "&client_assertion_type=unknown", null, null); + + "&client_assertion_type=unknown", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid client assertion type: unknown"); @@ -165,10 +185,13 @@ public void testAccessTokenRequestWithClientAssertionFailures() { KeyStore publicKeyProvider = Mockito.mock(KeyStore.class); try { + TokenConfigOptions tokenConfigOptions = new TokenConfigOptions(); + tokenConfigOptions.setPublicKeyProvider(publicKeyProvider); + tokenConfigOptions.setOauth2Issuer("https://athenz.io"); new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers" + "&authorization_details=details&expires_in=100&client_assertion=invalid-token" + "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer", - publicKeyProvider, "https://athenz.io"); + tokenConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertTrue(ex.getMessage().startsWith("Invalid client assertion: Unable to parse token: ")); @@ -201,10 +224,13 @@ public void testAccessTokenRequestWithClientAssertion() throws JOSEException { Mockito.when(publicKeyProvider.getServicePublicKey("athenz", "api", "eckey1")) .thenReturn(Crypto.loadPublicKey(ecPublicKey)); + TokenConfigOptions tokenConfigOptions = new TokenConfigOptions(); + tokenConfigOptions.setPublicKeyProvider(publicKeyProvider); + tokenConfigOptions.setOauth2Issuer("https://athenz.io/zts/v1"); AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers" + "&authorization_details=details&expires_in=100&client_assertion=" + token + "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer", - publicKeyProvider, "https://athenz.io/zts/v1"); + tokenConfigOptions); assertNotNull(request); assertEquals(request.getGrantType(), "client_credentials"); assertEquals(request.getScope(), "coretech:role.writers"); @@ -222,7 +248,7 @@ public void testAccessTokenRequestWithClientAssertion() throws JOSEException { public void testAccessTokenRequestQueryData() { AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials" - + "&scope=data\ntest\ragain", null, null); + + "&scope=data\ntest\ragain", defaultConfigOptions); assertNotNull(request); assertEquals(request.getQueryLogData(), "scope=data%0Atest%0Dagain"); @@ -230,7 +256,7 @@ public void testAccessTokenRequestQueryData() { final String scope = "012345678901234".repeat(67); request = new AccessTokenRequest("grant_type=client_credentials" - + "&scope=" + scope + "&expires_in=1024", null, null); + + "&scope=" + scope + "&expires_in=1024", defaultConfigOptions); assertEquals(request.getQueryLogData(), "scope=" + scope + "&expires_in=1"); } @@ -239,7 +265,7 @@ public void testAccessTokenRequestQueryDataWithAllFields() { AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials" + "&scope=test&expires_in=100&proxy_for_principal=user.joe" - + "&authorization_details=details&proxy_principal_spiffe_uris=spiffe://data/sa/service,spiffe://sports/sa/api", null, null); + + "&authorization_details=details&proxy_principal_spiffe_uris=spiffe://data/sa/service,spiffe://sports/sa/api", defaultConfigOptions); String queryData = request.getQueryLogData(); assertNotNull(queryData); assertTrue(queryData.contains("scope=test")); @@ -255,7 +281,7 @@ public void testAccessTokenRequestQueryDataWithAllFields() { public void testAccessTokenRequestWithProxyForPrincipal() { AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials" - + "&scope=test&proxy_for_principal=user.joe", null, null); + + "&scope=test&proxy_for_principal=user.joe", defaultConfigOptions); assertNotNull(request); assertEquals(request.getProxyForPrincipal(), "user.joe"); assertEquals(request.getScope(), "test"); @@ -266,17 +292,17 @@ public void testAccessTokenRequestWithProxyForPrincipal() { public void testAccessTokenRequestWithOpenIDIssuer() { AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials" - + "&scope=test&openid_issuer=true", null, null); + + "&scope=test&openid_issuer=true", defaultConfigOptions); assertNotNull(request); assertTrue(request.isUseOpenIDIssuer()); request = new AccessTokenRequest("grant_type=client_credentials" - + "&scope=test&openid_issuer=false", null, null); + + "&scope=test&openid_issuer=false", defaultConfigOptions); assertNotNull(request); assertFalse(request.isUseOpenIDIssuer()); request = new AccessTokenRequest("grant_type=client_credentials" - + "&scope=test&openid_issuer=invalid", null, null); + + "&scope=test&openid_issuer=invalid", defaultConfigOptions); assertNotNull(request); assertFalse(request.isUseOpenIDIssuer()); } @@ -285,17 +311,17 @@ public void testAccessTokenRequestWithOpenIDIssuer() { public void testAccessTokenRequestInvalidComponentParsing() { // component without separator - AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test&invalid", null, null); + AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test&invalid", defaultConfigOptions); assertNotNull(request); assertEquals(request.getScope(), "test"); // component with invalid URL encoding in key - request = new AccessTokenRequest("grant_type=client_credentials&scope=test&%ZZ=value", null, null); + request = new AccessTokenRequest("grant_type=client_credentials&scope=test&%ZZ=value", defaultConfigOptions); assertNotNull(request); assertEquals(request.getScope(), "test"); // component with invalid URL encoding in value - request = new AccessTokenRequest("grant_type=client_credentials&scope=test&extra=%ZZ", null, null); + request = new AccessTokenRequest("grant_type=client_credentials&scope=test&extra=%ZZ", defaultConfigOptions); assertNotNull(request); assertEquals(request.getScope(), "test"); } @@ -303,11 +329,19 @@ public void testAccessTokenRequestInvalidComponentParsing() { @Test public void testAccessTokenRequestTokenExchange() { + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" - + "&audience=sports&subject_token=token123" + + "&audience=sports&subject_token=" + subjectToken + "&scope=readers" - + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", null, null); + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); assertNotNull(request); assertEquals(request.getGrantType(), "urn:ietf:params:oauth:grant-type:token-exchange"); assertEquals(request.getRequestType(), AccessTokenRequest.RequestType.JAG_TOKEN_EXCHANGE); @@ -316,11 +350,18 @@ public void testAccessTokenRequestTokenExchange() { @Test public void testAccessTokenRequestTokenExchangeWithResource() { + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" - + "&audience=sports&resource=data&subject_token=token123" + + "&audience=sports&resource=data&subject_token=" + subjectToken + "&scope=readers" - + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", null, null); + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); assertNotNull(request); assertEquals(request.getGrantType(), "urn:ietf:params:oauth:grant-type:token-exchange"); assertEquals(request.getRequestType(), AccessTokenRequest.RequestType.JAG_TOKEN_EXCHANGE); @@ -334,7 +375,7 @@ public void testAccessTokenRequestTokenExchangeInvalidRequestedTokenType() { + "&requested_token_type=invalid" + "&audience=sports&subject_token=token123" + "&scope=readers" - + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", null, null); + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid requested token type: invalid"); @@ -342,28 +383,14 @@ public void testAccessTokenRequestTokenExchangeInvalidRequestedTokenType() { } @Test - public void testAccessTokenRequestTokenExchangeMissingRequestedTokenType() { - - try { - new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" - + "&audience=sports&subject_token=token123" - + "&scope=readers" - + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", null, null); - fail(); - } catch (IllegalArgumentException ex) { - assertTrue(ex.getMessage().startsWith("Invalid requested token type:")); - } - } - - @Test - public void testAccessTokenRequestTokenExchangeMissingAudience() { + public void testAccessTokenRequestJAGTokenExchangeMissingAudience() { try { new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" + "&subject_token=token123" + "&scope=readers" - + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", null, null); + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no audience provided"); @@ -371,14 +398,14 @@ public void testAccessTokenRequestTokenExchangeMissingAudience() { } @Test - public void testAccessTokenRequestTokenExchangeEmptyAudience() { + public void testAccessTokenRequestJAGTokenExchangeEmptyAudience() { try { new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" + "&audience=&subject_token=token123" + "&scope=readers" - + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", null, null); + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no audience provided"); @@ -386,14 +413,14 @@ public void testAccessTokenRequestTokenExchangeEmptyAudience() { } @Test - public void testAccessTokenRequestTokenExchangeMissingSubjectToken() { + public void testAccessTokenRequestTokenJAGExchangeMissingSubjectToken() { try { new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" + "&audience=sports" + "&scope=readers" - + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", null, null); + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no subject token provided"); @@ -401,14 +428,14 @@ public void testAccessTokenRequestTokenExchangeMissingSubjectToken() { } @Test - public void testAccessTokenRequestTokenExchangeEmptySubjectToken() { + public void testAccessTokenRequestTokenJAGExchangeEmptySubjectToken() { try { new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" + "&audience=sports&subject_token=" + "&scope=readers" - + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", null, null); + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no subject token provided"); @@ -423,7 +450,7 @@ public void testAccessTokenRequestTokenExchangeInvalidSubjectTokenType() { + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" + "&audience=sports&subject_token=token123" + "&scope=readers" - + "&subject_token_type=invalid", null, null); + + "&subject_token_type=invalid", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid subject token type: invalid"); @@ -437,7 +464,7 @@ public void testAccessTokenRequestTokenExchangeMissingSubjectTokenType() { new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" + "&scope=readers" - + "&audience=sports&subject_token=token123", null, null); + + "&audience=sports&subject_token=token123", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertTrue(ex.getMessage().startsWith("Invalid subject token type:")); @@ -448,7 +475,7 @@ public void testAccessTokenRequestTokenExchangeMissingSubjectTokenType() { public void testAccessTokenRequestMissingScope() { try { - new AccessTokenRequest("grant_type=client_credentials", null, null); + new AccessTokenRequest("grant_type=client_credentials", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no scope provided"); @@ -458,7 +485,7 @@ public void testAccessTokenRequestMissingScope() { @Test public void testAccessTokenRequestNullPrincipal() { - AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test", null, null); + AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test", defaultConfigOptions); assertNotNull(request); assertNull(request.getPrincipal()); } @@ -466,7 +493,7 @@ public void testAccessTokenRequestNullPrincipal() { @Test public void testAccessTokenRequestGettersWithDefaults() { - AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test", null, null); + AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test", defaultConfigOptions); assertNotNull(request); assertEquals(request.getGrantType(), "client_credentials"); assertEquals(request.getScope(), "test"); @@ -483,7 +510,7 @@ public void testAccessTokenRequestGettersWithDefaults() { public void testAccessTokenRequestQueryDataWithEmptyProxyPrincipalsSpiffeUris() { AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test" - + "&proxy_principal_spiffe_uris=", null, null); + + "&proxy_principal_spiffe_uris=", defaultConfigOptions); String queryData = request.getQueryLogData(); assertNotNull(queryData); assertTrue(queryData.contains("scope=test")); @@ -493,7 +520,7 @@ public void testAccessTokenRequestQueryDataWithEmptyProxyPrincipalsSpiffeUris() @Test public void testAccessTokenRequestQueryDataNoExpiryTime() { - AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test", null, null); + AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test", defaultConfigOptions); String queryData = request.getQueryLogData(); assertNotNull(queryData); assertTrue(queryData.contains("scope=test")); @@ -503,13 +530,21 @@ public void testAccessTokenRequestQueryDataNoExpiryTime() { @Test public void testAccessTokenRequestJWTBearer() { + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String assertionToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, AccessToken.HDR_TOKEN_JAG); + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" - + "&assertion=jwt-token-value" - + "&scope=test&resource=data", null, null); + + "&assertion=" + assertionToken + + "&scope=test&resource=data", defaultConfigOptions); assertNotNull(request); assertEquals(request.getGrantType(), "urn:ietf:params:oauth:grant-type:jwt-bearer"); assertEquals(request.getRequestType(), AccessTokenRequest.RequestType.JAG_JWT_BEARER); - assertEquals(request.getAssertion(), "jwt-token-value"); + assertEquals(request.getAssertion(), assertionToken); assertEquals(request.getScope(), "test"); assertEquals(request.getResource(), "data"); } @@ -519,7 +554,7 @@ public void testAccessTokenRequestJWTBearerMissingAssertion() { try { new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" - + "&scope=test", null, null); + + "&scope=test", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no assertion provided"); @@ -531,7 +566,7 @@ public void testAccessTokenRequestJWTBearerEmptyAssertion() { try { new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" - + "&assertion=&scope=test", null, null); + + "&assertion=&scope=test", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no assertion provided"); @@ -542,7 +577,7 @@ public void testAccessTokenRequestJWTBearerEmptyAssertion() { public void testAccessTokenRequestNoGrantType() { try { - new AccessTokenRequest("scope=test", null, null); + new AccessTokenRequest("scope=test", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no grant type provided"); @@ -553,7 +588,7 @@ public void testAccessTokenRequestNoGrantType() { public void testAccessTokenRequestEmptyGrantType() { try { - new AccessTokenRequest("grant_type=&scope=test", null, null); + new AccessTokenRequest("grant_type=&scope=test", defaultConfigOptions); fail(); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "Invalid request: no grant type provided"); @@ -563,19 +598,28 @@ public void testAccessTokenRequestEmptyGrantType() { @Test public void testAccessTokenRequestTokenExchangeGetters() { + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" - + "&audience=sports&subject_token=token123&scope=writers" + + "&audience=sports&subject_token=" + subjectToken + "&scope=writers" + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" - + "&actor_token=actor123&actor_token_type=urn:ietf:params:oauth:token-type:id_token", null, null); + + "&actor_token=" + subjectToken + "&actor_token_type=urn:ietf:params:oauth:token-type:id_token", + defaultConfigOptions); assertNotNull(request); assertEquals(request.getGrantType(), "urn:ietf:params:oauth:grant-type:token-exchange"); assertEquals(request.getRequestType(), AccessTokenRequest.RequestType.JAG_TOKEN_EXCHANGE); assertEquals(request.getRequestedTokenType(), "urn:ietf:params:oauth:token-type:id-jag"); assertEquals(request.getAudience(), "sports"); - assertEquals(request.getSubjectToken(), "token123"); + assertEquals(request.getSubjectToken(), subjectToken); assertEquals(request.getSubjectTokenType(), "urn:ietf:params:oauth:token-type:id_token"); - assertEquals(request.getActorToken(), "actor123"); + assertEquals(request.getActorToken(), subjectToken); assertEquals(request.getActorTokenType(), "urn:ietf:params:oauth:token-type:id_token"); } @@ -605,10 +649,13 @@ public void testAccessTokenRequestWithClientAssertionGetters() throws JOSEExcept Mockito.when(publicKeyProvider.getServicePublicKey("athenz", "api", "eckey1")) .thenReturn(Crypto.loadPublicKey(ecPublicKey)); + TokenConfigOptions tokenConfigOptions = new TokenConfigOptions(); + tokenConfigOptions.setPublicKeyProvider(publicKeyProvider); + tokenConfigOptions.setOauth2Issuer("https://athenz.io/zts/v1"); AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers" + "&client_assertion=" + token + "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer", - publicKeyProvider, "https://athenz.io/zts/v1"); + tokenConfigOptions); assertNotNull(request); assertEquals(request.getClientAssertion(), token); assertEquals(request.getClientAssertionType(), "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); @@ -617,7 +664,7 @@ public void testAccessTokenRequestWithClientAssertionGetters() throws JOSEExcept @Test public void testAccessTokenRequestInvalidExpiryTime() { - AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test&expires_in=invalid", null, null); + AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test&expires_in=invalid", defaultConfigOptions); assertNotNull(request); assertEquals(request.getExpiryTime(), 0); } @@ -626,7 +673,7 @@ public void testAccessTokenRequestInvalidExpiryTime() { public void testAccessTokenRequestUpperCaseValues() { AccessTokenRequest request = new AccessTokenRequest("grant_type=CLIENT_CREDENTIALS&scope=TEST:ROLE.WRITERS" - + "&proxy_for_principal=USER.JOE", null, null); + + "&proxy_for_principal=USER.JOE", defaultConfigOptions); assertNotNull(request); assertEquals(request.getGrantType(), "client_credentials"); assertEquals(request.getScope(), "test:role.writers"); @@ -636,13 +683,449 @@ public void testAccessTokenRequestUpperCaseValues() { @Test public void testAccessTokenRequestTokenExchangeWithActorToken() { + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" - + "&audience=sports&scope=writers&subject_token=token123" + + "&audience=sports&scope=writers&subject_token=" + subjectToken + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" - + "&actor_token=actor123&actor_token_type=actor_type", null, null); + + "&actor_token=actor123&actor_token_type=actor_type", defaultConfigOptions); assertNotNull(request); assertEquals(request.getActorToken(), "actor123"); assertEquals(request.getActorTokenType(), "actor_type"); } + + @Test + public void testAccessTokenRequestTokenExchangeWithAccessTokenType() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&requested_token_type=urn:ietf:params:oauth:token-type:access_token" + + "&audience=sports&subject_token=" + subjectToken + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); + assertNotNull(request); + assertEquals(request.getGrantType(), "urn:ietf:params:oauth:grant-type:token-exchange"); + assertEquals(request.getRequestType(), AccessTokenRequest.RequestType.TOKEN_EXCHANGE); + assertEquals(request.getRequestedTokenType(), "urn:ietf:params:oauth:token-type:access_token"); + } + + @Test + public void testAccessTokenRequestTokenExchangeWithEmptyRequestedTokenType() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&audience=sports&subject_token=" + subjectToken + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); + assertNotNull(request); + assertEquals(request.getGrantType(), "urn:ietf:params:oauth:grant-type:token-exchange"); + assertEquals(request.getRequestType(), AccessTokenRequest.RequestType.TOKEN_EXCHANGE); + assertTrue(StringUtil.isEmpty(request.getRequestedTokenType())); + } + + @Test + public void testAccessTokenRequestTokenExchangeWithAccessTokenSubjectTokenType() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&audience=sports&subject_token=" + subjectToken + + "&subject_token_type=urn:ietf:params:oauth:token-type:access_token", defaultConfigOptions); + assertNotNull(request); + assertEquals(request.getSubjectTokenType(), "urn:ietf:params:oauth:token-type:access_token"); + } + + @Test + public void testAccessTokenRequestTokenExchangeWithJWTSubjectTokenType() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&audience=sports&subject_token=" + subjectToken + + "&subject_token_type=urn:ietf:params:oauth:token-type:jwt", defaultConfigOptions); + assertNotNull(request); + assertEquals(request.getSubjectTokenType(), "urn:ietf:params:oauth:token-type:jwt"); + } + + @Test + public void testAccessTokenRequestTokenExchangeWithInvalidSubjectTokenType() { + + try { + new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&audience=sports&subject_token=token123" + + "&subject_token_type=invalid", defaultConfigOptions); + fail(); + } catch (IllegalArgumentException ex) { + assertEquals(ex.getMessage(), "Invalid subject token type: invalid"); + } + } + + @Test + public void testAccessTokenRequestTokenExchangeWithActorTokenMissingType() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + + try { + new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&audience=sports&subject_token=" + subjectToken + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + + "&actor_token=actor123", defaultConfigOptions); + fail(); + } catch (IllegalArgumentException ex) { + assertEquals(ex.getMessage(), "Invalid actor token type: null"); + } + } + + @Test + public void testAccessTokenRequestTokenExchangeWithActorTokenInvalidType() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + final File ecPublicKey = new File("./src/test/resources/zts_public_ec.pem"); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "athenz.api", + "athenz.api", expiryTime, null); + + KeyStore publicKeyProvider = Mockito.mock(KeyStore.class); + Mockito.when(publicKeyProvider.getServicePublicKey("sys.auth", "zts", "0")) + .thenReturn(Crypto.loadPublicKey(ecPublicKey)); + + try { + defaultConfigOptions.setOauth2Issuer("https://athenz.io:4443/zts/v1"); + defaultConfigOptions.setPublicKeyProvider(publicKeyProvider); + new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&audience=sports&subject_token=" + subjectToken + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + + "&actor_token=" + subjectToken + "&actor_token_type=invalid", defaultConfigOptions); + fail(); + } catch (IllegalArgumentException ex) { + assertEquals(ex.getMessage(), "Invalid actor token type: invalid"); + } + } + + @Test + public void testAccessTokenRequestTokenExchangeWithActorTokenInvalidToken() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + + KeyStore publicKeyProvider = Mockito.mock(KeyStore.class); + try { + TokenConfigOptions tokenConfigOptions = new TokenConfigOptions(); + tokenConfigOptions.setPublicKeyProvider(publicKeyProvider); + tokenConfigOptions.setOauth2Issuer("https://athenz.io"); + tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); + tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); + new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&audience=sports&subject_token=" + subjectToken + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + + "&actor_token=invalid-token&actor_token_type=urn:ietf:params:oauth:token-type:id_token", + tokenConfigOptions); + fail(); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().startsWith("Invalid actor token: Unable to parse token: ")); + } + } + + @Test + public void testAccessTokenRequestTokenExchangeWithValidActorToken() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + final File ecPublicKey = new File("./src/test/resources/zts_public_ec.pem"); + + long now = System.currentTimeMillis() / 1000; + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + KeyStore publicKeyProvider = Mockito.mock(KeyStore.class); + Mockito.when(publicKeyProvider.getServicePublicKey("athenz", "api", "eckey1")) + .thenReturn(Crypto.loadPublicKey(ecPublicKey)); + Mockito.when(publicKeyProvider.getServicePublicKey("sys.auth", "zts", "0")) + .thenReturn(Crypto.loadPublicKey(ecPublicKey)); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = now + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + String actorToken = createToken(privateKey, "0", "athenz.api", + "athenz.api", expiryTime, null); + + TokenConfigOptions tokenConfigOptions = new TokenConfigOptions(); + tokenConfigOptions.setPublicKeyProvider(publicKeyProvider); + tokenConfigOptions.setOauth2Issuer("https://athenz.io:4443/zts/v1"); + tokenConfigOptions.setJwtJAGProcessor(createJAGProcessor()); + tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&audience=sports&subject_token=" + subjectToken + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + + "&actor_token=" + actorToken + + "&actor_token_type=urn:ietf:params:oauth:token-type:id_token", + tokenConfigOptions); + assertNotNull(request); + assertEquals(request.getActorToken(), actorToken); + assertNotNull(request.getActorTokenObj()); + } + + @Test + public void testAccessTokenRequestComponentWithNullValue() { + + // Component with null value after decoding should be skipped + AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test&extra=%ZZ", defaultConfigOptions); + assertNotNull(request); + assertEquals(request.getScope(), "test"); + } + + @Test + public void testAccessTokenRequestEmptyBody() { + + try { + new AccessTokenRequest("", defaultConfigOptions); + fail(); + } catch (IllegalArgumentException ex) { + assertEquals(ex.getMessage(), "Invalid request: no grant type provided"); + } + } + + @Test + public void testAccessTokenRequestQueryDataEmpty() { + + AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test", defaultConfigOptions); + String queryData = request.getQueryLogData(); + assertNotNull(queryData); + assertTrue(queryData.contains("scope=test")); + } + + @Test + public void testAccessTokenRequestGetActorTokenObj() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&audience=sports&subject_token=" + subjectToken + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); + assertNotNull(request); + assertNull(request.getActorTokenObj()); + } + + @Test + public void testAccessTokenRequestJAGTokenExchangeWithClientAssertion() throws JOSEException { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + final File ecPublicKey = new File("./src/test/resources/zts_public_ec.pem"); + + long now = System.currentTimeMillis() / 1000; + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey); + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .subject("athenz.api") + .issueTime(Date.from(Instant.ofEpochSecond(now))) + .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600))) + .issuer("athenz.api") + .audience("https://athenz.io/zts/v1") + .build(); + + SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("eckey1").build(), claimsSet); + signedJWT.sign(signer); + final String token = signedJWT.serialize(); + + KeyStore publicKeyProvider = Mockito.mock(KeyStore.class); + Mockito.when(publicKeyProvider.getServicePublicKey("athenz", "api", "eckey1")) + .thenReturn(Crypto.loadPublicKey(ecPublicKey)); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = now + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + + TokenConfigOptions tokenConfigOptions = new TokenConfigOptions(); + tokenConfigOptions.setPublicKeyProvider(publicKeyProvider); + tokenConfigOptions.setOauth2Issuer("https://athenz.io/zts/v1"); + tokenConfigOptions.setJwtIDTProcessor(createIDTokenProcessor()); + AccessTokenRequest request = new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&requested_token_type=urn:ietf:params:oauth:token-type:id-jag" + + "&audience=sports&scope=readers&subject_token=" + subjectToken + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token" + + "&client_assertion=" + token + + "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + tokenConfigOptions); + assertNotNull(request); + assertNotNull(request.getPrincipal()); + } + + @Test + public void testAccessTokenRequestTokenExchangeMissingSubjectToken() { + + try { + new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&requested_token_type=urn:ietf:params:oauth:token-type:access_token" + + "&audience=sports" + + "&scope=readers" + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); + fail(); + } catch (IllegalArgumentException ex) { + assertEquals(ex.getMessage(), "Invalid request: no subject token provided"); + } + } + + @Test + public void testAccessTokenRequestTokenExchangeEmptySubjectToken() { + + try { + new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&requested_token_type=urn:ietf:params:oauth:token-type:access_token" + + "&audience=sports&subject_token=" + + "&scope=readers" + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); + fail(); + } catch (IllegalArgumentException ex) { + assertEquals(ex.getMessage(), "Invalid request: no subject token provided"); + } + } + + @Test + public void testAccessTokenRequestTokenExchangeMissingAudience() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + + try { + new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&requested_token_type=urn:ietf:params:oauth:token-type:access_token" + + "&subject_token=" + subjectToken + + "&scope=readers" + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); + fail(); + } catch (IllegalArgumentException ex) { + assertEquals(ex.getMessage(), "Invalid request: no audience provided"); + } + } + + @Test + public void testAccessTokenRequestTokenExchangeEmptyAudience() { + + final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem"); + PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); + + // Create a subject token for user_domain.user with audience as proxy-user1 + long expiryTime = System.currentTimeMillis() / 1000 + 3600; + String subjectToken = createToken(privateKey, "0", "user_domain.user", + "user_domain.proxy-user1", expiryTime, null); + + try { + new AccessTokenRequest("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + + "&requested_token_type=urn:ietf:params:oauth:token-type:access_token" + + "&audience=&subject_token=" + subjectToken + + "&scope=readers" + + "&subject_token_type=urn:ietf:params:oauth:token-type:id_token", defaultConfigOptions); + fail(); + } catch (IllegalArgumentException ex) { + assertEquals(ex.getMessage(), "Invalid request: no audience provided"); + } + } + + private String createToken(PrivateKey privateKey, String keyId, String subject, + String audience, long expiryTime, String tokenType) { + try { + JWSSigner signer = JwtsHelper.getJWSSigner(privateKey); + long now = System.currentTimeMillis() / 1000; + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .subject(subject) + .issueTime(Date.from(Instant.ofEpochSecond(now))) + .expirationTime(Date.from(Instant.ofEpochSecond(expiryTime))) + .issuer("https://athenz.io:4443/zts/v1") + .audience(audience) + .claim("ver", 1) + .claim("auth_time", now) + .build(); + + JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(keyId); + if (tokenType != null) { + builder.type(new JOSEObjectType(tokenType)); + } + SignedJWT signedJWT = new SignedJWT(builder.build(), claimsSet); + signedJWT.sign(signer); + return signedJWT.serialize(); + } catch (JOSEException ex) { + fail("Failed to create ID token: " + ex.getMessage()); + return null; + } + } + + private ConfigurableJWTProcessor createJAGProcessor() { + + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); + JwtsSigningKeyResolver resolver = new JwtsSigningKeyResolver(jwksUri, null, null, true); + + ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSTypeVerifier(JwtsHelper.JWT_JAG_TYPE_VERIFIER); + + jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(JwtsHelper.JWS_SUPPORTED_ALGORITHMS, + resolver.getKeySource())); + return jwtProcessor; + } + + private ConfigurableJWTProcessor createIDTokenProcessor() { + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); + JwtsSigningKeyResolver resolver = new JwtsSigningKeyResolver(jwksUri, null, null, true); + + ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(JwtsHelper.JWS_SUPPORTED_ALGORITHMS, + resolver.getKeySource())); + return jwtProcessor; + } } diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/TokenConfigOptionsTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/TokenConfigOptionsTest.java new file mode 100644 index 00000000000..335d324d710 --- /dev/null +++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/TokenConfigOptionsTest.java @@ -0,0 +1,198 @@ +/* + * Copyright The Athenz Authors + * + * 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 com.yahoo.athenz.zts.token; + +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.yahoo.athenz.auth.KeyStore; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import static org.testng.Assert.*; + +public class TokenConfigOptionsTest { + + @Test + public void testDefaultConstructor() { + TokenConfigOptions options = new TokenConfigOptions(); + assertNotNull(options); + assertNull(options.getPublicKeyProvider()); + assertNull(options.getOauth2Issuer()); + assertNull(options.getJwtIDTProcessor()); + assertNull(options.getJwtJAGProcessor()); + } + + @Test + public void testPublicKeyProviderGetterAndSetter() { + TokenConfigOptions options = new TokenConfigOptions(); + + // Initially null + assertNull(options.getPublicKeyProvider()); + + // Set a mock KeyStore + KeyStore mockKeyStore = Mockito.mock(KeyStore.class); + options.setPublicKeyProvider(mockKeyStore); + assertNotNull(options.getPublicKeyProvider()); + assertEquals(options.getPublicKeyProvider(), mockKeyStore); + + // Set to null + options.setPublicKeyProvider(null); + assertNull(options.getPublicKeyProvider()); + + // Set a different KeyStore + KeyStore anotherKeyStore = Mockito.mock(KeyStore.class); + options.setPublicKeyProvider(anotherKeyStore); + assertEquals(options.getPublicKeyProvider(), anotherKeyStore); + } + + @Test + public void testOauth2IssuerGetterAndSetter() { + TokenConfigOptions options = new TokenConfigOptions(); + + // Initially null + assertNull(options.getOauth2Issuer()); + + // Set a value + String issuer = "https://athenz.io/zts/v1"; + options.setOauth2Issuer(issuer); + assertNotNull(options.getOauth2Issuer()); + assertEquals(options.getOauth2Issuer(), issuer); + + // Set to null + options.setOauth2Issuer(null); + assertNull(options.getOauth2Issuer()); + + // Set a different value + String anotherIssuer = "https://example.com/oauth2"; + options.setOauth2Issuer(anotherIssuer); + assertEquals(options.getOauth2Issuer(), anotherIssuer); + + // Set empty string + options.setOauth2Issuer(""); + assertEquals(options.getOauth2Issuer(), ""); + } + + @Test + public void testJwtIDTProcessorGetterAndSetter() { + TokenConfigOptions options = new TokenConfigOptions(); + + // Initially null + assertNull(options.getJwtIDTProcessor()); + + // Set a mock processor + @SuppressWarnings("unchecked") + ConfigurableJWTProcessor mockProcessor = + Mockito.mock(ConfigurableJWTProcessor.class); + options.setJwtIDTProcessor(mockProcessor); + assertNotNull(options.getJwtIDTProcessor()); + assertEquals(options.getJwtIDTProcessor(), mockProcessor); + + // Set to null + options.setJwtIDTProcessor(null); + assertNull(options.getJwtIDTProcessor()); + + // Set a different processor + @SuppressWarnings("unchecked") + ConfigurableJWTProcessor anotherProcessor = + Mockito.mock(ConfigurableJWTProcessor.class); + options.setJwtIDTProcessor(anotherProcessor); + assertEquals(options.getJwtIDTProcessor(), anotherProcessor); + } + + @Test + public void testJwtJAGProcessorGetterAndSetter() { + TokenConfigOptions options = new TokenConfigOptions(); + + // Initially null + assertNull(options.getJwtJAGProcessor()); + + // Set a mock processor + @SuppressWarnings("unchecked") + ConfigurableJWTProcessor mockProcessor = + Mockito.mock(ConfigurableJWTProcessor.class); + options.setJwtJAGProcessor(mockProcessor); + assertNotNull(options.getJwtJAGProcessor()); + assertEquals(options.getJwtJAGProcessor(), mockProcessor); + + // Set to null + options.setJwtJAGProcessor(null); + assertNull(options.getJwtJAGProcessor()); + + // Set a different processor + @SuppressWarnings("unchecked") + ConfigurableJWTProcessor anotherProcessor = + Mockito.mock(ConfigurableJWTProcessor.class); + options.setJwtJAGProcessor(anotherProcessor); + assertEquals(options.getJwtJAGProcessor(), anotherProcessor); + } + + @Test + public void testAllFieldsSetAndGet() { + TokenConfigOptions options = new TokenConfigOptions(); + + KeyStore mockKeyStore = Mockito.mock(KeyStore.class); + String issuer = "https://athenz.io/zts/v1"; + @SuppressWarnings("unchecked") + ConfigurableJWTProcessor mockIDTProcessor = + Mockito.mock(ConfigurableJWTProcessor.class); + @SuppressWarnings("unchecked") + ConfigurableJWTProcessor mockJAGProcessor = + Mockito.mock(ConfigurableJWTProcessor.class); + + // Set all fields + options.setPublicKeyProvider(mockKeyStore); + options.setOauth2Issuer(issuer); + options.setJwtIDTProcessor(mockIDTProcessor); + options.setJwtJAGProcessor(mockJAGProcessor); + + // Verify all fields are set correctly + assertEquals(options.getPublicKeyProvider(), mockKeyStore); + assertEquals(options.getOauth2Issuer(), issuer); + assertEquals(options.getJwtIDTProcessor(), mockIDTProcessor); + assertEquals(options.getJwtJAGProcessor(), mockJAGProcessor); + } + + @Test + public void testIndependentFields() { + TokenConfigOptions options = new TokenConfigOptions(); + + KeyStore mockKeyStore = Mockito.mock(KeyStore.class); + @SuppressWarnings("unchecked") + ConfigurableJWTProcessor mockIDTProcessor = + Mockito.mock(ConfigurableJWTProcessor.class); + @SuppressWarnings("unchecked") + ConfigurableJWTProcessor mockJAGProcessor = + Mockito.mock(ConfigurableJWTProcessor.class); + + // Set IDT processor + options.setJwtIDTProcessor(mockIDTProcessor); + assertNotNull(options.getJwtIDTProcessor()); + assertNull(options.getJwtJAGProcessor()); + + // Set JAG processor independently + options.setJwtJAGProcessor(mockJAGProcessor); + assertNotNull(options.getJwtIDTProcessor()); + assertNotNull(options.getJwtJAGProcessor()); + assertNotEquals(options.getJwtIDTProcessor(), options.getJwtJAGProcessor()); + + // Set public key provider independently + options.setPublicKeyProvider(mockKeyStore); + assertNotNull(options.getPublicKeyProvider()); + assertNotNull(options.getJwtIDTProcessor()); + assertNotNull(options.getJwtJAGProcessor()); + } +} +