Skip to content

Commit

Permalink
[SDK-4401] Support using organization name (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmyjames committed Jul 17, 2023
1 parent 93ce224 commit 982c8b8
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 17 deletions.
10 changes: 5 additions & 5 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,23 @@ Note that Organizations is currently only available to customers on our Enterpri

### Log in to an organization

Log in to an organization by using `withOrganization()` when configuring the `AuthenticationController`:
Log in to an organization by using `withOrganization()` when configuring the `AuthenticationController`, passing either the organization ID or organization name:

```java
AuthenticationController controller = AuthenticationController.newBuilder("YOUR-AUTH0-DOMAIN", "YOUR-CLIENT-ID", "YOUR-CLIENT-SECRET")
.withOrganization("{ORG_ID}")
.build();
```

When logging into an organization, this library will validate that the `org_id` claim of the ID Token matches the value configured.
When logging into an organization, this library will validate that the `org_id` or `org_name` claim of the ID Token matches the value configured.

If no organization parameter was given to the authorization endpoint, but an `org_id` claim is present in the ID Token, then the claim should be validated by the application to ensure that the value received is expected or known.
If no organization parameter was given to the authorization endpoint, but an `org_id` or `org_name` claim is present in the ID Token, then the claim should be validated by the application to ensure that the value received is expected or known.

Normally, validating the issuer would be enough to ensure that the token was issued by Auth0, and this check is performed by this SDK.
In the case of organizations, additional checks may be required so that the organization within an Auth0 tenant is expected.

In particular, the `org_id` claim should be checked to ensure it is a value that is already known to the application.
This could be validated against a known list of organization IDs, or perhaps checked in conjunction with the current request URL (e.g., the sub-domain may hint at what organization should be used to validate the ID Token).
In particular, the `org_id` or `org_name` claim should be checked to ensure it is a value that is already known to the application.
This could be validated against a known list of organizations, or perhaps checked in conjunction with the current request URL (e.g., the sub-domain may hint at what organization should be used to validate the ID Token).

If the claim cannot be validated, then the application should deem the token invalid.
The following example demonstrates this, using the [java-jwt](https://github.com/auth0/java-jwt) library:
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/auth0/AuthenticationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public Builder withLegacySameSiteCookie(boolean useLegacySameSiteCookie) {
/**
* Sets the organization query string parameter value used to login to an organization.
*
* @param organization The ID of the organization to log the user in to.
* @param organization The ID or name of the organization to log the user in to.
* @return the builder instance.
*/
public Builder withOrganization(String organization) {
Expand Down
27 changes: 21 additions & 6 deletions src/main/java/com/auth0/IdTokenVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class IdTokenVerifier {

/**
* Verifies a provided ID Token follows the OIDC specification.
* See https://openid.net/specs/openid-connect-core-1_0-final.html#IDTokenValidation
* @see <a href="https://openid.net/specs/openid-connect-core-1_0-final.html#IDTokenValidation">Open ID Connect Specification</a>
*
* @param token the ID Token to verify.
* @param verifyOptions the verification options, like audience, issuer, algorithm.
Expand Down Expand Up @@ -57,15 +57,30 @@ void verify(String token, Options verifyOptions) throws TokenValidationException

// validate org if set
if (verifyOptions.organization != null) {
String orgIdClaim = decoded.getClaim("org_id").asString();
if (isEmpty(orgIdClaim)) {
throw new TokenValidationException("Organization Id (org_id) claim must be a string present in the ID token");
String org = verifyOptions.organization.trim();
if (org.startsWith("org_")) {
// org ID
String orgIdClaim = decoded.getClaim("org_id").asString();
if (isEmpty(orgIdClaim)) {
throw new TokenValidationException("Organization Id (org_id) claim must be a string present in the ID token");
}
if (!org.equals(orgIdClaim)) {
throw new TokenValidationException(String.format("Organization (org_id) claim mismatch in the ID token; expected \"%s\" but found \"%s\"", verifyOptions.organization, orgIdClaim));
}
}
if (!verifyOptions.organization.equals(orgIdClaim)) {
throw new TokenValidationException(String.format("Organization (org_id) claim mismatch in the ID token; expected \"%s\" but found \"%s\"", verifyOptions.organization, orgIdClaim));
else {
// org name
String orgNameClaim = decoded.getClaim("org_name").asString();
if (isEmpty(orgNameClaim)) {
throw new TokenValidationException("Organization name (org_name) claim must be a string present in the ID token");
}
if (!org.equalsIgnoreCase(orgNameClaim)) {
throw new TokenValidationException(String.format("Organization (org_name) claim mismatch in the ID token; expected \"%s\" but found \"%s\"", verifyOptions.organization, orgNameClaim));
}
}
}

// TODO refactor to modern date/time APIs
final Calendar cal = Calendar.getInstance();
final Date now = verifyOptions.clock != null ? verifyOptions.clock : cal.getTime();
final int clockSkew = verifyOptions.clockSkew != null ? verifyOptions.clockSkew : DEFAULT_CLOCK_SKEW;
Expand Down
144 changes: 139 additions & 5 deletions src/test/java/com/auth0/IdTokenVerifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,121 @@ public void succeedsWithValidTokenUsingDefaultClockAndHttpsDomain() {
}

@Test
public void succeedsWhenOrganizationMatchesExpected() {
public void succeedsWhenOrganizationNameMatchesExpected() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
.withIssuedAt(getYesterday())
.withExpiresAt(getTomorrow())
.withIssuer("https://" + DOMAIN + "/")
.withClaim("org_name", "my org")
.sign(Algorithm.HMAC256("secret"));

String jwt = JWT.decode(token).getToken();

IdTokenVerifier.Options opts = configureOptions(jwt);
opts.setOrganization("my org");

new IdTokenVerifier().verify(token, opts);
}

@Test
public void failsWhenOrganizationNameDoesNotMatchExpected() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
.withIssuedAt(getYesterday())
.withExpiresAt(getTomorrow())
.withIssuer("https://" + DOMAIN + "/")
.withClaim("org_name", "my org")
.sign(Algorithm.HMAC256("secret"));

String jwt = JWT.decode(token).getToken();

IdTokenVerifier.Options opts = configureOptions(jwt);
opts.setOrganization("other org");

TokenValidationException e = assertThrows(TokenValidationException.class, () -> new IdTokenVerifier().verify(token, opts));
assertEquals("Organization (org_name) claim mismatch in the ID token; expected \"other org\" but found \"my org\"", e.getMessage());
}

@Test
public void succeedsWhenOrganizationNameDoesNotMatchExpected_caseInsensitive() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
.withIssuedAt(getYesterday())
.withExpiresAt(getTomorrow())
.withIssuer("https://" + DOMAIN + "/")
.withClaim("org_name", "my org")
.sign(Algorithm.HMAC256("secret"));

String jwt = JWT.decode(token).getToken();

IdTokenVerifier.Options opts = configureOptions(jwt);
opts.setOrganization("My org");

new IdTokenVerifier().verify(token, opts);
}

@Test
public void failsWhenOrganizationNameExpectedButNotPresent() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
.withIssuedAt(getYesterday())
.withExpiresAt(getTomorrow())
.withIssuer("https://" + DOMAIN + "/")
.sign(Algorithm.HMAC256("secret"));

String jwt = JWT.decode(token).getToken();

IdTokenVerifier.Options opts = configureOptions(jwt);
opts.setOrganization("my org");

TokenValidationException e = assertThrows(TokenValidationException.class, () -> new IdTokenVerifier().verify(token, opts));
assertEquals("Organization name (org_name) claim must be a string present in the ID token", e.getMessage());
}

@Test
public void failsWhenOrganizationNameExpectedButClaimIsNotString() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
.withIssuedAt(getYesterday())
.withExpiresAt(getTomorrow())
.withIssuer("https://" + DOMAIN + "/")
.withClaim("org_name", 42)
.sign(Algorithm.HMAC256("secret"));

String jwt = JWT.decode(token).getToken();

IdTokenVerifier.Options opts = configureOptions(jwt);
opts.setOrganization("my org");

TokenValidationException e = assertThrows(TokenValidationException.class, () -> new IdTokenVerifier().verify(token, opts));
assertEquals("Organization name (org_name) claim must be a string present in the ID token", e.getMessage());
}

@Test
public void succeedsWhenOrganizationNameNotSpecifiedButIsPresent() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
.withIssuedAt(getYesterday())
.withExpiresAt(getTomorrow())
.withIssuer("https://" + DOMAIN + "/")
.withClaim("org_name", "my org")
.sign(Algorithm.HMAC256("secret"));

String jwt = JWT.decode(token).getToken();

IdTokenVerifier.Options opts = configureOptions(jwt);
new IdTokenVerifier().verify(token, opts);
}

@Test
public void succeedsWhenOrganizationIdMatchesExpected() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
Expand All @@ -445,7 +559,7 @@ public void succeedsWhenOrganizationMatchesExpected() {
}

@Test
public void failsWhenOrganizationDoesNotMatchExpected() {
public void failsWhenOrganizationIdDoesNotMatchExpected() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
Expand All @@ -465,7 +579,27 @@ public void failsWhenOrganizationDoesNotMatchExpected() {
}

@Test
public void failsWhenOrganizationExpectedButNotPresent() {
public void failsWhenOrganizationIdDoesNotMatchExpected_caseSensitive() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
.withIssuedAt(getYesterday())
.withExpiresAt(getTomorrow())
.withIssuer("https://" + DOMAIN + "/")
.withClaim("org_id", "org_123")
.sign(Algorithm.HMAC256("secret"));

String jwt = JWT.decode(token).getToken();

IdTokenVerifier.Options opts = configureOptions(jwt);
opts.setOrganization("org_aBc");

TokenValidationException e = assertThrows(TokenValidationException.class, () -> new IdTokenVerifier().verify(token, opts));
assertEquals("Organization (org_id) claim mismatch in the ID token; expected \"org_aBc\" but found \"org_123\"", e.getMessage());
}

@Test
public void failsWhenOrganizationIdExpectedButNotPresent() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
Expand All @@ -484,7 +618,7 @@ public void failsWhenOrganizationExpectedButNotPresent() {
}

@Test
public void failsWhenOrganizationExpectedButClaimIsNotString() {
public void failsWhenOrganizationIdExpectedButClaimIsNotString() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
Expand All @@ -504,7 +638,7 @@ public void failsWhenOrganizationExpectedButClaimIsNotString() {
}

@Test
public void succeedsWhenOrganizationNotSpecifiedButIsPresent() {
public void succeedsWhenOrganizationIdNotSpecifiedButIsPresent() {
String token = JWT.create()
.withSubject("auth0|sdk458fks")
.withAudience(AUDIENCE)
Expand Down

0 comments on commit 982c8b8

Please sign in to comment.