Skip to content

Commit

Permalink
Add support for organizations feature (#338)
Browse files Browse the repository at this point in the history
* Add support for organizations feature

* review feedback - improved docs, null check for org_id claim value, additional tests
  • Loading branch information
jimmyjames authored Mar 25, 2021
1 parent 359c2cd commit 868fcd7
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 1 deletion.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,51 @@ try {
}
```

### Organizations (Closed Beta)

Organizations is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications.

Using Organizations, you can:

- Represent teams, business customers, partner companies, or any logical grouping of users that should have different ways of accessing your applications, as organizations.
- Manage their membership in a variety of ways, including user invitation.
- Configure branded, federated login flows for each organization.
- Implement role-based access control, such that users can have different roles when authenticating in the context of different organizations.
- Build administration capabilities into your products, using Organizations APIs, so that those businesses can manage their own organizations.

Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans.

#### Log in to an organization

Log in to an organization by using `withOrganization()` when building the Authorization URL:

```java
AuthAPI auth = new AuthAPI("{YOUR_DOMAIN}", "{YOUR_CLIENT_ID}", "{YOUR_CLIENT_SECRET}");
String url = auth.authorizeUrl("https://me.auth0.com/callback")
.withOrganization("{YOUR_ORGANIZATION_ID")
.build();
```

> When logging into an organization, it is important to ensure the `org_id` claim of the ID Token matches the expected organization value. The `IdTokenVerifier` can be configured with an expected `org_id` claim value:
>```java
>IdTokenVerifier.init("{ISSUER}", "{AUDIENCE}", signatureVerifier)
> .withOrganization("{ORG_ID}")
> .build()
> .verify(jwt);
>```
### Accept user invitations
Accept a user invitation by using `withInvitation()` when building the Authorization URL:
```
AuthAPI auth = new AuthAPI("{YOUR_DOMAIN}", "{YOUR_CLIENT_ID}", "{YOUR_CLIENT_SECRET}");
String url = auth.authorizeUrl("https://me.auth0.com/callback")
.withOrganization("{YOUR_ORGANIZATION_ID")
.withInvitation("{YOUR_INVITATION_ID}")
.build();
```
## Management API
The implementation is based on the [Management API Docs](https://auth0.com/docs/api/management/v2).
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/com/auth0/client/auth/AuthorizeUrlBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,31 @@ public AuthorizeUrlBuilder withResponseType(String responseType) {
return this;
}

/**
* 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.
* @return the builder instance.
*/
public AuthorizeUrlBuilder withOrganization(String organization) {
assertNotNull(organization, "organization");
parameters.put("organization", organization);
return this;
}

/**
* Sets the invitation query string parameter to join an organization. If using this, you must also specify the
* organization using {@linkplain AuthorizeUrlBuilder#withOrganization(String)}.
*
* @param invitation The ID of the invitation to accept. This is available on the URL that is provided when accepting an invitation.
* @return the builder instance.
*/
public AuthorizeUrlBuilder withInvitation(String invitation) {
assertNotNull(invitation, "invitation");
parameters.put("invitation", invitation);
return this;
}

/**
* Sets an additional parameter.
*
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/com/auth0/utils/tokens/IdTokenVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ public final class IdTokenVerifier {
private final Integer leeway;
private final Date clock;
private final SignatureVerifier signatureVerifier;
private final String organization;

private IdTokenVerifier(Builder builder) {
this.issuer = builder.issuer;
this.audience = builder.audience;
this.leeway = builder.leeway;
this.signatureVerifier = builder.signatureVerifier;
this.clock = builder.clock;
this.organization = builder.organization;
}

/**
Expand Down Expand Up @@ -134,6 +136,18 @@ public void verify(String token, String nonce, Integer maxAuthenticationAge) thr
throw new IdTokenValidationException(String.format("Audience (aud) claim mismatch in the ID token; expected \"%s\" but found \"%s\"", this.audience, decoded.getAudience()));
}

// Org verification
if (this.organization != null) {
String orgClaim = decoded.getClaim("org_id").asString();
if (isEmpty(orgClaim)) {
throw new IdTokenValidationException("Organization Id (org_id) claim must be a string present in the ID token");
}
if (!this.organization.equals(orgClaim)) {
throw new IdTokenValidationException(String.format("Organization (org_id) claim mismatch in the ID token; expected \"%s\" but found \"%s\"", this.organization, orgClaim));
}

}

final Calendar cal = Calendar.getInstance();
final Date now = this.clock != null ? this.clock : cal.getTime();
final int clockSkew = this.leeway != null ? this.leeway : DEFAULT_LEEWAY;
Expand Down Expand Up @@ -209,6 +223,7 @@ public static class Builder {

private Integer leeway;
private Date clock;
private String organization;

/**
* Create a new Builder instance.
Expand Down Expand Up @@ -251,6 +266,18 @@ Builder withClock(Date clock) {
return this;
}

/**
* Specify the expected organization (org_id) the token must be issued for. This should be used if using the
* Organizations feature.
* @param organization the ID of the organization.
* @return this Builder instance.
*/
Builder withOrganization(String organization) {
this.organization = organization;
return this;
}

/**
* Constructs an {@linkplain IdTokenVerifier} instance from this Builder.
*
Expand Down
34 changes: 34 additions & 0 deletions src/test/java/com/auth0/client/auth/AuthorizeUrlBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,38 @@ public void shouldThrowWhenCustomParameterValueIsNull() {
AuthorizeUrlBuilder.newInstance(DOMAIN, CLIENT_ID, REDIRECT_URI)
.withParameter("name", null);
}

@Test
public void shouldAddOrganizationParameter() {
String authUrl = AuthorizeUrlBuilder.newInstance(DOMAIN, CLIENT_ID, REDIRECT_URI)
.withOrganization("org_abc")
.build();
assertThat(authUrl, hasQueryParameter("organization", "org_abc"));
}

@Test
public void shouldThrowWhenOrganizationIsNull() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("'organization' cannot be null!");
AuthorizeUrlBuilder.newInstance(DOMAIN, CLIENT_ID, REDIRECT_URI)
.withOrganization(null)
.build();
}

@Test
public void shouldAddInvitationParameter() {
String authUrl = AuthorizeUrlBuilder.newInstance(DOMAIN, CLIENT_ID, REDIRECT_URI)
.withInvitation("invitation_123")
.build();
assertThat(authUrl, hasQueryParameter("invitation", "invitation_123"));
}

@Test
public void shouldThrowWhenInvitationIsNull() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("'invitation' cannot be null!");
AuthorizeUrlBuilder.newInstance(DOMAIN, CLIENT_ID, REDIRECT_URI)
.withInvitation(null)
.build();
}
}
104 changes: 103 additions & 1 deletion src/test/java/com/auth0/utils/tokens/IdTokenVerifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,108 @@ public void succeedsWithValidTokenUsingDefaultClockAndHttpsDomain() {
.verify(token, "nonce");
}

@Test
public void succeedsWhenOrganizationMatchesExpected() {
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();

configureVerifier(jwt)
.withOrganization("org_123")
.build()
.verify(jwt);
}

@Test
public void failsWhenOrganizationDoesNotMatchExpected() {
exception.expect(IdTokenValidationException.class);
exception.expectMessage("Organization (org_id) claim mismatch in the ID token; expected \"org_abc\" but found \"org_123\"");

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();

configureVerifier(jwt)
.withOrganization("org_abc")
.build()
.verify(jwt);
}

@Test
public void failsWhenOrganizationExpectedButNotPresent() {
exception.expect(IdTokenValidationException.class);
exception.expectMessage("Organization Id (org_id) claim must be a string present in the ID token");

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();

configureVerifier(jwt)
.withOrganization("org_123")
.build()
.verify(jwt);
}

@Test
public void failsWhenOrganizationExpectedButClaimIsNotString() {
exception.expect(IdTokenValidationException.class);
exception.expectMessage("Organization Id (org_id) claim must be a string present in the ID token");

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

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

configureVerifier(jwt)
.withOrganization("org_123")
.build()
.verify(jwt);
}

@Test
public void succeedsWhenOrganizationNotSpecifiedButIsPresent() {
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();

configureVerifier(jwt)
.build()
.verify(jwt);
}

private IdTokenVerifier.Builder configureVerifier(String token) {
DecodedJWT decodedJWT = JWT.decode(token);
SignatureVerifier verifier = mock(SignatureVerifier.class);
Expand All @@ -469,4 +571,4 @@ private Date getTomorrow() {

return cal.getTime();
}
}
}

0 comments on commit 868fcd7

Please sign in to comment.