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
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.auth;

import com.yahoo.athenz.auth.token.OAuth2Token;

import java.util.List;

public interface TokenExchangeIdentityProvider {

/**
* Return the corresponding athenz identity for the principal identity
* from the given token. The token has already been validated by the server.
* This could be used when issuing JAG tokens and the subject token is issued
* by an external Identity Provider. Similarly, it could be used when exchanging
* JAG tokens from an external Identity Provider with an Athenz issued access
* token.
*
* @param token validated oauth2 token from external Identity Provider
* @return the identity of the token in Athenz system.
*/
String getTokenIdentity(OAuth2Token token);

/**
* Return the audience value to be used for the token exchange.
* Typically, if this is an ID token then the audience would be
* included in the aud claim. However, if this is an access token
* then the audience might be a different value and the actual client
* id would be included in the cid or a different claim.
*
* @param token validated oauth2 token from external Identity Provider
* @return the audience value
*/
String getTokenAudience(OAuth2Token token);

/**
* Return the list of claims that should be included in the
* generated token as part of the exchange request in addition
* to the standard claims (iss, sub, aud, exp, iat, scp).
* @return list of claim names
*/
List<String> getTokenExchangeClaims();
}
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ public String getSignedToken(final PrivateKey key, final String keyId, final Str

try {
JWSSigner signer = JwtsHelper.getJWSSigner(key);
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder()
.subject(subject)
.jwtID(jwtId)
.issueTime(Date.from(Instant.ofEpochSecond(issueTime)))
Expand All @@ -521,9 +521,13 @@ public String getSignedToken(final PrivateKey key, final String keyId, final Str
.claim(CLAIM_CONFIRM, confirm)
.claim(CLAIM_PROXY, proxyPrincipal)
.claim(CLAIM_AUTHZ_DETAILS, authorizationDetails)
.claim(CLAIM_RESOURCE, resource)
.build();

.claim(CLAIM_RESOURCE, resource);
if (customClaims != null) {
for (Map.Entry<String, Object> entry : customClaims.entrySet()) {
claimsSetBuilder.claim(entry.getKey(), entry.getValue());
}
}
JWTClaimsSet claimsSet = claimsSetBuilder.build();
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.parse(sigAlg))
.type(new JOSEObjectType(tokenType))
Expand All @@ -541,4 +545,24 @@ public String getSignedToken(final PrivateKey key, final String keyId, final Str
public String getSignedToken(final PrivateKey key, final String keyId, final String sigAlg) {
return getSignedToken(key, keyId, sigAlg, HDR_TOKEN_JWT);
}

@Override
public boolean isStandardClaim(final String claimName) {
if (super.isStandardClaim(claimName)) {
return true;
}
switch (claimName) {
case CLAIM_SCOPE:
case CLAIM_SCOPE_STD:
case CLAIM_UID:
case CLAIM_CLIENT_ID:
case CLAIM_CONFIRM:
case CLAIM_PROXY:
case CLAIM_AUTHZ_DETAILS:
case CLAIM_RESOURCE:
return true;
default:
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;

public class IdToken extends OAuth2Token {

Expand Down Expand Up @@ -107,7 +108,7 @@ public String getSignedToken(final PrivateKey key, final String keyId, final Str

try {
JWSSigner signer = JwtsHelper.getJWSSigner(key);
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder()
.subject(subject)
.issueTime(Date.from(Instant.ofEpochSecond(issueTime)))
.expirationTime(Date.from(Instant.ofEpochSecond(expiryTime)))
Expand All @@ -116,9 +117,13 @@ public String getSignedToken(final PrivateKey key, final String keyId, final Str
.claim(CLAIM_AUTH_TIME, authTime)
.claim(CLAIM_VERSION, version)
.claim(CLAIM_GROUPS, groups)
.claim(CLAIM_NONCE, nonce)
.build();

.claim(CLAIM_NONCE, nonce);
if (customClaims != null) {
for (Map.Entry<String, Object> entry : customClaims.entrySet()) {
claimsSetBuilder.claim(entry.getKey(), entry.getValue());
}
}
JWTClaimsSet claimsSet = claimsSetBuilder.build();
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.parse(sigAlg))
.keyID(keyId)
Expand All @@ -131,4 +136,18 @@ public String getSignedToken(final PrivateKey key, final String keyId, final Str
return null;
}
}

@Override
public boolean isStandardClaim(final String claimName) {
if (super.isStandardClaim(claimName)) {
return true;
}
switch (claimName) {
case CLAIM_GROUPS:
case CLAIM_NONCE:
return true;
default:
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.security.PublicKey;
import java.util.Date;
import java.util.List;
import java.util.Map;

public class OAuth2Token {

Expand All @@ -36,6 +37,13 @@ public class OAuth2Token {

public static final String CLAIM_VERSION = "ver";
public static final String CLAIM_AUTH_TIME = "auth_time";
public static final String CLAIM_ISSUER = "iss";
public static final String CLAIM_SUBJECT = "sub";
public static final String CLAIM_AUDIENCE = "aud";
public static final String CLAIM_EXPIRY = "exp";
public static final String CLAIM_ISSUE_TIME = "iat";
public static final String CLAIM_NOT_BEFORE = "nbf";
public static final String CLAIM_JWT_ID = "jti";

protected int version;
protected long expiryTime;
Expand All @@ -49,6 +57,7 @@ public class OAuth2Token {
protected String clientIdDomainName;
protected String clientIdServiceName;
protected JWTClaimsSet claimsSet = null;
protected Map<String, Object> customClaims = null;
protected static DefaultJWTClaimsVerifier<SecurityContext> claimsVerifier = new DefaultJWTClaimsVerifier<>(null, null);

public OAuth2Token() {
Expand Down Expand Up @@ -358,4 +367,49 @@ public String getClientIdDomainName() {
public String getClientIdServiceName() {
return clientIdServiceName;
}

public boolean setCustomClaim(final String name, final Object value) {

// first verify that the custom claim is not one of the standard claims

if (isStandardClaim(name)) {
return false;
}

// create the custom claims map if necessary

if (customClaims == null) {
customClaims = new java.util.HashMap<>();
}
customClaims.put(name, value);
return true;
}

public Object getClaim(final String name) {
return claimsSet.getClaim(name);
}

/**
* Check if the given claim name is one of the standard claims
* in an OAuth2 token that is already handled separately.
*
* @param claimName claim name
* @return true if standard claim, false otherwise
*/
public boolean isStandardClaim(final String claimName) {
switch (claimName) {
case CLAIM_ISSUER:
case CLAIM_SUBJECT:
case CLAIM_AUDIENCE:
case CLAIM_EXPIRY:
case CLAIM_ISSUE_TIME:
case CLAIM_NOT_BEFORE:
case CLAIM_JWT_ID:
case CLAIM_VERSION:
case CLAIM_AUTH_TIME:
return true;
default:
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,55 @@ public void testAccessToken() throws JOSEException, ParseException {
assertEquals(scopes.get(0), "readers");
}

@Test
public void testAccessTokenWtihCustomClaims() throws JOSEException, ParseException {

long now = System.currentTimeMillis() / 1000;

AccessToken accessToken = createAccessToken(now);

// custom claims should return true
assertTrue(accessToken.setCustomClaim("preferred_email", "noreply@athenz.io"));
String[] emails = new String[] {"noreply1@athenz.io", "noreply2@athenz.io"};
assertTrue(accessToken.setCustomClaim("emails", emails));

// standard claims should return failure

assertFalse(accessToken.setCustomClaim(AccessToken.CLAIM_SCOPE, "admins"));
assertFalse(accessToken.setCustomClaim(AccessToken.CLAIM_SUBJECT, "subject"));

// verify the getters

validateAccessToken(accessToken, now);

// now get the signed token

PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
String accessJws = accessToken.getSignedToken(privateKey, "eckey1", "ES256");
assertNotNull(accessJws);

// now verify our signed token

PublicKey publicKey = Crypto.loadPublicKey(ecPublicKey);
JWSVerifier verifier = new ECDSAVerifier((ECPublicKey) publicKey);
SignedJWT signedJWT = SignedJWT.parse(accessJws);
assertTrue(signedJWT.verify(verifier));
JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
assertNotNull(claimsSet);

assertEquals(claimsSet.getSubject(), "subject");
assertEquals(JwtsHelper.getAudience(claimsSet), "coretech");
assertEquals(claimsSet.getIssuer(), "athenz");
assertEquals(claimsSet.getJWTID(), "jwt-id001");
assertEquals(claimsSet.getStringClaim("scope"), "readers");
List<String> scopes = claimsSet.getStringListClaim("scp");
assertNotNull(scopes);
assertEquals(scopes.size(), 1);
assertEquals(scopes.get(0), "readers");
assertEquals(claimsSet.getClaim("preferred_email"), "noreply@athenz.io");
assertEquals(claimsSet.getClaim("emails"), Arrays.asList(emails));
}

@Test
public void testAccessTokenMultipleRoles() throws JOSEException, ParseException {

Expand Down Expand Up @@ -255,6 +304,7 @@ public void testAccessTokenWithX509Cert() throws IOException {
X509Certificate cert = Crypto.loadX509Certificate(certStr);

AccessToken checkToken = new AccessToken(accessJws, resolver, cert);
assertEquals(checkToken.getClaim("sub"), "subject");
validateAccessToken(checkToken, now);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.security.interfaces.ECPublicKey;
import java.text.ParseException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Objects;
Expand Down Expand Up @@ -107,6 +108,49 @@ public void testIdToken() throws JOSEException, ParseException {
assertEquals(claimsSet.getIssuer(), "athenz");
}

@Test
public void testIdTokenCustomClaims() throws JOSEException, ParseException {

long now = System.currentTimeMillis() / 1000;

IdToken token = createIdToken(now);

// custom claims should return true
assertTrue(token.setCustomClaim("preferred_email", "noreply@athenz.io"));
String[] emails = new String[] {"noreply1@athenz.io", "noreply2@athenz.io"};
assertTrue(token.setCustomClaim("emails", emails));

// standard claims should return failure

assertFalse(token.setCustomClaim(IdToken.CLAIM_NONCE, "nonce"));
assertFalse(token.setCustomClaim(IdToken.CLAIM_SUBJECT, "subject"));

// verify the getters

validateIdToken(token, now);

// now get the signed token

PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
String idJws = token.getSignedToken(privateKey, "eckey1", "ES256");
assertNotNull(idJws);

// now verify our signed token

PublicKey publicKey = Crypto.loadPublicKey(ecPublicKey);
JWSVerifier verifier = new ECDSAVerifier((ECPublicKey) publicKey);
SignedJWT signedJWT = SignedJWT.parse(idJws);
assertTrue(signedJWT.verify(verifier));
JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
assertNotNull(claimsSet);

assertEquals(claimsSet.getSubject(), "subject");
assertEquals(claimsSet.getAudience().get(0), "coretech");
assertEquals(claimsSet.getIssuer(), "athenz");
assertEquals(claimsSet.getClaim("preferred_email"), "noreply@athenz.io");
assertEquals(claimsSet.getClaim("emails"), Arrays.asList(emails));
}

@Test
public void testIdTokenSignedToken() {

Expand Down
17 changes: 9 additions & 8 deletions servers/zts/conf/zts.properties
Original file line number Diff line number Diff line change
Expand Up @@ -632,14 +632,15 @@ athenz.zts.cert_signer_factory_class=com.yahoo.athenz.zts.cert.impl.SelfCertSign
# The uri must not contain a trailing /.
#athenz.zts.oidc_port_issuer=

# Comma separated list of trusted external JWT issuers that ZTS will trust
# for JWT Authorization Grant (JAG) tokens. Each issuer entry is specified in the format:
# <issuer_url>[|<default_jwks_uri>|<proxy_url>] where:
# - issuer_url: The issuer URL to trust (required)
# - default_jwks_uri: Default JWKS URI to use if not found in openid-configuration (optional)
# - proxy_url: Proxy URL to use when fetching JWKS (optional)
# Multiple issuers can be specified separated by commas.
#athenz.zts.openid_jag_issuers=
# Path to a file that contains the support list of external Identity
# Provider JWT issuers that ZTS will trust for JWT Authorization
# Grant (JAG) tokens. The file must list of provider config objects
# where each object has the following fields:
# - issuerUri: The issuer URI for the provider
# - jwksUri: The JWKS URI for the provider (as default in case we can't extract from issuerUri)
# - proxyUrl: Optional proxy URL for the provider
# - providerClassName: The class name of the TokenExchangeIdentityProvider implementation
#athenz.zts.oauth_provider_config_file=

# The path to the trust store file that contains CA certificates
# trusted by the ZTS Provider Client (this client is used to validate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public final class ZTSConsts {
public static final String ZTS_PROP_OIDC_PORT_ISSUER = "athenz.zts.oidc_port_issuer";
public static final String ZTS_PROP_REDIRECT_URI_SUFFIX = "athenz.zts.redirect_uri_suffix";
public static final String ZTS_PROP_SCOPE_ROLE_WOUT_DOMAIN = "athenz.zts.oauth_scope_role_without_domain";
public static final String ZTS_PROP_OPENID_JAG_ISSUERS = "athenz.zts.openid_jag_issuers";
public static final String ZTS_PROP_PROVIDER_CONFIG_FILE = "athenz.zts.oauth_provider_config_file";

public static final String ZTS_PROP_CERTSIGN_BASE_URI = "athenz.zts.certsign_base_uri";
public static final String ZTS_PROP_CERTSIGN_REQUEST_TIMEOUT = "athenz.zts.certsign_request_timeout";
Expand Down
Loading
Loading