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
34 changes: 30 additions & 4 deletions servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3231,11 +3231,37 @@ AccessTokenResponse processJAGTokenExchangeRequest(ResourceContext ctx, Principa
}

// now we need to validate the scope value

if (StringUtil.isEmpty(jagToken.getScopeStd())) {
//
// Mandatory Scope: While RFC 7523 defines the 'scope' parameter as
// optional for JAG token, Athenz strictly requires it to determine which Athenz role
// the issued Access Token should represent.
//
// Downscoping: Per RFC 7521, if a requested 'scope' is included in the
// exchange request, it MUST be equal to or less than the scope originally
// granted to the given ID-JAG token. The authorization server (Athenz) MUST limit
// the scope of the issued access token to be a subset of the original scope.

String scopeStd = jagToken.getScopeStd();
if (StringUtil.isBlank(scopeStd)) {
LOGGER.error("Invalid jag assertion - missing scope");
throw requestError("Invalid jag assertion - missing scope", caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, clientPrincipalDomain);
}
String requestedScope = accessTokenRequest.getScope();
if (!StringUtil.isBlank(requestedScope)) {
final String trimmedRequestedScope = requestedScope.trim();
final Set<String> requestedSet = new LinkedHashSet<>(Arrays.asList(trimmedRequestedScope.split("\\s+")));
final Set<String> assertionSet = new LinkedHashSet<>(Arrays.asList(scopeStd.split("\\s+")));

if (!assertionSet.containsAll(requestedSet)) {
LOGGER.error("Requested scope is not a subset of assertion scope. Requested: {}, Assertion: {}", requestedSet, assertionSet);
throw requestError("Invalid request: requested scope is not a subset of assertion scope",
caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, clientPrincipalDomain);
}

// The authorization server MUST limit the scope of the issued access token
// to be equal to or less than the scope originally granted.
scopeStd = String.join(" ", requestedSet);
}

// get our principal name for simpler access. with jag token that is specified
// as the subject in the token and not the principal who was authenticated
Expand All @@ -3250,13 +3276,13 @@ AccessTokenResponse processJAGTokenExchangeRequest(ResourceContext ctx, Principa
}

if (LOGGER.isDebugEnabled()) {
LOGGER.debug("processAccessTokenJAGRequest(principal: {}, scope: {})", principalName, jagToken.getScopeStd());
LOGGER.debug("processAccessTokenJAGRequest(principal: {}, assertionScope: {}, requestedScope: {}, scope: {})", principalName, jagToken.getScopeStd(), requestedScope, scopeStd);
}

// our scopes are space separated list of values

final String principalDomain = AthenzUtils.extractPrincipalDomainName(principalName);
AccessTokenScope tokenScope = new AccessTokenScope(jagToken.getScopeStd(), principalDomain);
AccessTokenScope tokenScope = new AccessTokenScope(scopeStd, principalDomain);

// before using any of our values let's validate that they
// match our schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.testng.annotations.DataProvider;

import java.io.*;
import java.net.URLEncoder;
Expand Down Expand Up @@ -2052,8 +2053,69 @@ public void testProcessJAGTokenExchangeRequestSuccessWithOpenIDIssuer() throws J
System.clearProperty(ZTSConsts.ZTS_PROP_OAUTH_ISSUER);
}

@Test
public void testProcessJAGTokenExchangeRequestSuccessWithSpecificRoles() {
@DataProvider(name = "jagTokenExchangeCases")
public Object[][] jagTokenExchangeCases() {

final String W_USER = "user_domain.user";
final String WR_USER = "user_domain.user1";
final String W = "coretech:role.writers";
final String R = "coretech:role.readers";
final String W_R = "coretech:role.writers coretech:role.readers";


return new Object[][] {
// { gotUser, gotJAGScope, gotRequestedScope, wantScope, wantErrorCode, wantErrorMessage }
{ W_USER, W, null, null, 0, null },
{ W_USER, W, "", null, 0, null },
{ W_USER, W, W, null, 0, null },
{ W_USER, W, W_R, null, 400, "Invalid request: requested scope is not a subset of assertion scope" },
{ W_USER, W, R, null, 400, "Invalid request: requested scope is not a subset of assertion scope" },

{ W_USER, W_R, null, W, 0, null },
{ W_USER, W_R, "", W, 0, null },
{ W_USER, W_R, W, null, 0, null },
{ W_USER, W_R, W_R, W, 0, null },
{ W_USER, W_R, R, null, 403, "principal user_domain.user is not included in the requested role(s) in domain coretech" },

{ W_USER, R, null, null, 403, "principal user_domain.user is not included in the requested role(s) in domain coretech" },
{ W_USER, R, "", null, 403, "principal user_domain.user is not included in the requested role(s) in domain coretech" },
{ W_USER, R, W, null, 400, "Invalid request: requested scope is not a subset of assertion scope" },
{ W_USER, R, W_R, null, 400, "Invalid request: requested scope is not a subset of assertion scope" },
{ W_USER, R, R, null, 403, "principal user_domain.user is not included in the requested role(s) in domain coretech" },

{ WR_USER, W, null, null, 0, null },
{ WR_USER, W, "", null, 0, null },
{ WR_USER, W, W, null, 0, null },
{ WR_USER, W, W_R, null, 400, "Invalid request: requested scope is not a subset of assertion scope" },
{ WR_USER, W, R, null, 400, "Invalid request: requested scope is not a subset of assertion scope" },

{ WR_USER, W_R, null, null, 0, null },
{ WR_USER, W_R, "", null, 0, null },
{ WR_USER, W_R, W, null, 0, null },
{ WR_USER, W_R, W_R, null, 0, null },
{ WR_USER, W_R, R, null, 0, null },

{ WR_USER, R, null, null, 0, null },
{ WR_USER, R, "", null, 0, null },
{ WR_USER, R, W, null, 400, "Invalid request: requested scope is not a subset of assertion scope" },
{ WR_USER, R, W_R, null, 400, "Invalid request: requested scope is not a subset of assertion scope" },
{ WR_USER, R, R, null, 0, null },

// edge cases:
{ W_USER, null, null, null, 400, "Invalid jag assertion - missing scope" }, // no scope in jag assertion
{ W_USER, " ", null, null, 400, "Invalid jag assertion - missing scope" }, // white spaces in jag assertion
{ W_USER, W, " ", null, 0, null }, // white spaces in requested scope acts the same as empty requested scope
};
}

@Test(dataProvider = "jagTokenExchangeCases")
public void testProcessJAGTokenExchangeRequest(
String gotUser,
String gotJAGScope,
String gotRequestedScope,
String wantScope,
int wantErrorCode,
String wantErrorMessage) {

System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");

Expand All @@ -2069,23 +2131,44 @@ public void testProcessJAGTokenExchangeRequestSuccessWithSpecificRoles() {
File privateKeyFile = new File("src/test/resources/unit_test_zts_private_ec.pem");
PrivateKey privateKey = Crypto.loadPrivateKey(privateKeyFile);
long expiryTime = System.currentTimeMillis() / 1000 + 3600;
String jagToken = createJagToken(privateKey, "0", "user_domain.user", "coretech.jwt",
"coretech:role.writers", ztsImpl.ztsOAuthIssuer, expiryTime);
String jagToken = createJagToken(privateKey, "0", gotUser, "coretech.jwt",
gotJAGScope, ztsImpl.ztsOAuthIssuer, expiryTime);

HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class);
Mockito.when(servletRequest.isSecure()).thenReturn(true);
Principal principal = SimplePrincipal.create("coretech", "jwt",
"v=U1;d=coretech;n=jwt;s=signature", 0, null);
ResourceContext context = createResourceContext(principal);

AccessTokenResponse resp = ztsImpl.postAccessTokenRequest(context,
"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" + jagToken
+ "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
+ "&client_assertion=" + createClientAssertionToken(privateKey));
StringBuilder requestBody = new StringBuilder(
"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer"
+ "&assertion=" + jagToken
+ "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
+ "&client_assertion=" + createClientAssertionToken(privateKey));

assertNotNull(resp);
assertNull(resp.getScope()); // No scope returned when specific role requested
assertNotNull(resp.getAccess_token());
if (gotRequestedScope != null) {
requestBody.append("&scope=").append(gotRequestedScope);
}

try {
AccessTokenResponse resp = ztsImpl.postAccessTokenRequest(context, requestBody.toString());

if (wantErrorCode != 0) {
fail("Expected error code " + wantErrorCode + " but request succeeded");
}

assertNotNull(resp);
assertEquals(resp.getScope(), wantScope);
assertNotNull(resp.getAccess_token());

} catch (ResourceException ex) {
if (wantErrorCode == 0) {
fail("Expected success but got error: " + ex.getMessage());
}

assertEquals(ex.getCode(), wantErrorCode);
if (!ex.getMessage().contains(wantErrorMessage)) {
fail("Want error: " + wantErrorMessage + ", Got error: " + ex.getMessage());
}
}
}

@Test
Expand Down Expand Up @@ -2237,44 +2320,6 @@ public void testProcessJAGTokenExchangeRequestInvalidClientId() {
}
}

@Test
public void testProcessJAGTokenExchangeRequestMissingScope() {

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.setJwtJAGProcessor(createJAGProcessor());
System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");

SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true);
store.processSignedDomain(signedDomain, false);

// Create JAG token without scope
File privateKeyFile = new File("src/test/resources/unit_test_zts_private_ec.pem");
PrivateKey privateKey = Crypto.loadPrivateKey(privateKeyFile);
long expiryTime = System.currentTimeMillis() / 1000 + 3600;
String jagToken = createJagToken(privateKey, "0", "user_domain.user", "coretech.jwt",
"", ztsImpl.ztsOAuthIssuer, expiryTime);

HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class);
Mockito.when(servletRequest.isSecure()).thenReturn(true);
Principal principal = SimplePrincipal.create("coretech", "jwt",
"v=U1;d=coretech;n=jwt;s=signature", 0, null);
ResourceContext context = createResourceContext(principal);

try {
ztsImpl.postAccessTokenRequest(context,
"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" + jagToken
+ "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
+ "&client_assertion=" + createClientAssertionToken(privateKey));
fail();
} catch (ResourceException ex) {
assertEquals(ex.getCode(), 400);
assertTrue(ex.getMessage().contains("Invalid jag assertion - missing scope"));
}
}

@Test
public void testProcessJAGTokenExchangeRequestMissingSubject() {

Expand Down
Loading