Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 39 additions & 39 deletions servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.*;
Expand Down Expand Up @@ -197,8 +193,7 @@ public class ZTSImpl implements ZTSHandler {
protected SecretKey serviceCredsEncryptionKey = null;
protected String serviceCredsEncryptionAlgorithm = null;
protected boolean jwtCurveRfcSupportOnly = false;
protected ConfigurableJWTProcessor<SecurityContext> jwtIDTProcessor = null;
protected ConfigurableJWTProcessor<SecurityContext> jwtJAGProcessor = null;
protected TokenConfigOptions tokenConfigOptions = null;

private static final String TYPE_DOMAIN_NAME = "DomainName";
private static final String TYPE_SIMPLE_NAME = "SimpleName";
Expand Down Expand Up @@ -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<JwtsResolver> 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() {
Expand Down Expand Up @@ -492,16 +506,6 @@ private void setupMetaConfigObjects() {
oauthConfig.setToken_endpoint_auth_signing_alg_values_supported(getSupportedSigningAlgValues());
}

protected void loadJWTProcessors() {
List<JwtsResolver> 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<JwtsResolver> generateSupportedJAGIssuers() {

// extract jag issuers if configured, the format is a comma separated
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -2639,28 +2643,31 @@ 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) {

// get our principal name for simpler access

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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,6 +42,7 @@ public class AccessTokenRequest {

public enum RequestType {
ACCESS_TOKEN,
TOKEN_EXCHANGE,
JAG_TOKEN_EXCHANGE,
JAG_JWT_BEARER
}
Expand All @@ -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";
Expand All @@ -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) {
Expand Down Expand Up @@ -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:
Expand All @@ -202,15 +219,15 @@ 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:
throw new IllegalArgumentException("Invalid grant request: " + grantType);
}
}

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
Expand All @@ -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
Expand All @@ -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());
Comment thread
havetisyan marked this conversation as resolved.
} 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)) {

Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
Loading
Loading