Rate this page

Authentication and authorization

Authentication and authorization are two related but distinct concepts that are central to the Ping Identity Data Governance Broker and its relationships to client applications. Let’s first define what these two words mean. We’ll use the deliberately ambiguous example of a driver’s license to illustrate both their differences and their affinities.

OAuth 2 and OpenID Connect

The Data Governance Broker authenticates users and authorizes clients. It does so by offering OAuth 2 and OpenID Connect services to clients. Clients make OAuth 2/OpenID Connect requests by redirecting the user’s web browser to the Data Governance Broker’s authorization endpoint, specifying the level of authorization that they want in the form of scopes. Scopes are simply strings that are mutually understood by the Data Governance Broker and the client to represent particular units of access to data. (For example, a scope called email might represent access to a user’s email address.) When the authorization endpoint receives a request from a client, it prompts the end user to log in and grant consent to these scopes, authorizing the request. The Data Governance Broker then returns a response containing an access token and (often) an ID token to a callback URI belonging to the client.

The access token acts as a credential which the client will then use to make requests to the Data Governance Broker’s SCIM service for data belonging to the user. The ID token gives the client information about the user’s authentication state. This includes the user’s unique ID, the time of authentication, and the level of authentication, as well as other information.

When OAuth 2 and OpenID Connect are used, the authentication and authorization interactions take place between the user and the Data Governance Broker. In a sense, this process is a black box to the client. While the client has various request parameters at its disposal to influence what goes on during authentication and authorization, the client does not actually handle this interaction with the user. This ensures that your organization’s authentication and authorization policies are the responsibility of a single entity, the Data Governance Broker. This also does away with the need for your application to handle sensitive credentials, like passwords.

Further authentication and authorization topics are discussed in more detail in the following articles:

You are also encouraged to walk through the examples in the OAuth 2 and OpenID Connect API reference.

Making an OpenID Connect request

Let’s walk through an example of how to make an OpenID Connect request using Java and the authorization code grant type. For the sake of example, we’ll assume that the client is a Jersey web application. If you use some other language or web framework, don’t worry — the example is still broadly applicable.

Note that we will use very simplified error handling and will omit some boilerplate code, such as getters and setters.

Set the state

OAuth 2 and OpenID Connect allow a client to provide a state parameter, which the server passes back to the client in the redirect response. The purpose of this parameter is twofold:

  1. If the state value is reasonably difficult to predict, then it can be used as a form of CSRF protection.
  2. The client can use it to pass data back to itself. For example, the client can store a path that the user should be returned to after authentication response processing is complete.

Your client can do whatever you want with this value. In this example, we’ll use a scheme in which the state is encoded as a signed JWT. This example uses the Nimbus JOSE+JWT library for JWT signing and verification.

public static class State {
  public String rfp; // Request forgery protection. Set this to a random value.
  public Date iat; // Issued-at.
  public Date exp; // Expires.
  public String aud; // Audience.
  public URI returnUri;

  // Constructor omitted...
  // Getters and setters omitted...

  public JWSObject sign(String signingKey) throws JOSEException {
    JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
        .issueTime(iat)
        .expirationTime(exp)
        .audience(aud)
        .claim("rfp", rfp)
        .claim("return_uri", returnUri);
    SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256),
                                  builder.build());
    JWSSigner signer = new MACSigner(signingKey);
    jwt.sign(signer);
    return jwt;
  }

  public static JWT verify(String jwt, String signingKey) throws Exception {
    SignedJWT signedJWT = SignedJWT.parse(jwt);
    JWSVerifier verifier = new MACVerifier(signingKey);
    if (signedJWT.verify(verifier)) {
      if (! signedJWT.getJWTClaimsSet().getStringClaim("rfp").equals(expectedRfp)) {
        throw new Exception("unexpected 'rfp' claim value");
      }
      return signedJWT;
    } else {
      throw new Exception("state signature verification failed");
    }
  }
}

We’ll use this class in a moment.

Build the authentication request

Let’s assume that we have a class representing the client’s configuration.

public class Config {
  // The Data Governance Broker's base OAuth service URI.
  String baseUri = "https://example.com/oauth";

  // Client credentials.
  String clientId = "my-app";
  String clientSecret = "Srf9BGpgZqfu1TSI8gTFmX9in8B2Z1ox";

  // Registered redirect URI.
  String redirectUri = "https://my-app.com/callback";

  // Scopes to request.
  Set<String> scopes = new HashSet<>(Arrays.asList("openid", "email"));

  // A secret key used to sign the state JWT.
  String stateSigningKey = "secret-signing-key";

  public static Config instance() {
    // Let's pretend that this does something...
  }
}

To make an authentication request, all we have to do is to build a URI with our client ID, scopes, and a few other items. Then we’ll send the user’s browser to that URI.

public URI authenticationUri() throws Exception
{
  Config config = Config.instance();

  // The nonce needs to be some unpredictable value.
  SecureRandom random = new SecureRandom();
  String nonce = new BigInteger(130, random).toString(32);

  // This is the State class from the previous section.
  State state = new State();
  // Set state fields... (omitted)
  // Sign the state object.
  JWSObject stateJws = state.sign(config.getStateSigningKey());
  // Serialize the state object to a string JWT.
  String stateJwt = stateJws.serialize();

  // Save the state and nonce values to the session...

  // Build our authentication URI.
  return UriBuilder.fromUri(config.getBaseUri())
      .path("authorize")
      .queryParam("response_type", "code")
      .queryParam("client_id", config.getClientId())
      .queryParam("redirect_uri", config.getRedirectUri())
      .queryParam("state", stateJwt)
      .queryParam("nonce", nonce)
      .queryParam("scope", config.getScopes().stream().collect(Collectors.joining(" ")))
      .build();
}

How you direct the user’s web browser to the authentication URI is up to you. You could invite the user to click on a link or a button, or you could simply perform a redirect.

Handle the authentication response

The Data Governance Broker’s response will arrive via your application’s redirect URI. When your application receives a GET request at this URI, then it should invoke its authentication response handling.

Here’s a simplified response handler from a Jersey application. It first checks to see if the response is an error. If not, it looks for the state and code parameters. It validates the state value and then submits the authorization code to the Data Governance Broker’s token endpoint, expecting to receive a response with an access token and an ID token.

@Path("callback")
@GET
public Response handleCallback(@Context UriInfo uriInfo) throws Exception
{
  Config config = Config.instance();
  String returnUri;

  String expectedRfp = "...";
  String expectedState = "...";

  MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();

  // Check for an error.
  if (queryParams.get("error") != null) {
    throw new Exception("error in OpenID Connect response");
  }

  // Validate the state parameter.
  if (queryParams.get("state") != null) {
    String state = queryParams.getFirst("state");
    if (! state.equals(expectedState)) {
      throw new Exception("'state' did not match expected value");
    }
    JWT parsedState = State.verify(state, config.getStateSigningKey(), expectedRfp);
    returnUri = parsedState.getJWTClaimsSet().getStringClaim("return_uri");
    if (returnUri == null) {
      throw new Exception("expected 'return_uri' claim missing");
    }
  } else {
    throw new Exception("expected 'state' parameter missing");
  }

  // Exchange the authorization code for an access token and ID token.
  if (queryParams.get("code") != null) {
    TokenResponse tokenResponse =
        submitAuthorizationCode(queryParams.getFirst("code"));
    // Validate ID token...
  } else {
    throw new Exception("expected 'code' parameter missing");
  }

  // Redirect the user-agent to a URI stored in the state.
  return Response.seeOther(new URI(returnUri)).build();
}

The actual process of submitting an authorization code to the token endpoint is fairly simple. The client submits a small set of parameters using the application/x-www-form-urlencoded encoding to the token endpoint, along with its client credentials, encoded using HTTP basic authentication.

The following example uses the Jersey client.

public TokenResponse submitAuthorizationCode(String code) {
  Config config = Config.instance();

  HttpAuthenticationFeature basicAuth = HttpAuthenticationFeature.basicBuilder()
      .credentials(config.getClientId(), config.getClientSecret())
      .build();
  ClientConfig clientConfig = new ClientConfig();
  clientConfig.register(basicAuth);
  Client client = ClientBuilder.newClient(clientConfig);
  URI tokenEndpoint = UriBuilder.fromUri(config.getBaseUri())
      .path("token")
      .build();
  WebTarget target = client.target(tokenEndpoint);
  Form tokenRequest = new Form();
  tokenRequest.param("grant_type", "authorization_code");
  tokenRequest.param("code", code);
  tokenRequest.param("redirect_uri", config.getRedirectUri());

  return target.request(MediaType.APPLICATION_JSON_TYPE)
      .post(Entity.entity(tokenRequest, MediaType.APPLICATION_FORM_URLENCODED_TYPE),
            TokenResponse.class);
}

Here’s an example of a model class representing the Data Governance Broker’s token endpoint response. Note that it uses Jackson annotations.

public class TokenResponse {
  @JsonProperty("access_token")
  String accessToken;
  @JsonProperty("refresh_token")
  String refreshToken;
  @JsonProperty("id_token")
  String idToken;
  @JsonProperty("token_type")
  String tokenType;
  @JsonProperty("state")
  String state;
  @JsonProperty("expires_in")
  Date expiresIn;
  @JsonProperty("scope")
  Set<String> scopes;

  public void setExpiresIn(long expiresIn) {
    this.expiresIn = new Date(expiresIn);
  }

  public void setScopes(String scope) {
    if (scope != null) {
      scopes = Arrays.stream(scope.split(" ")).collect(Collectors.toSet());
    }
  }
}

The Authorization Endpoint

When the Data Governance Broker’s authorization endpoint receives an OAuth 2 or OpenID Connect request, it hands off processing to the Auth UI. This is a web application that is responsible for displaying the authentication and authorization interface to the user. The Auth UI acts as a client for the Data Governance Broker’s private Authentication API, which directs the authentication flow, consisting of a chain of authentication operations handled by server-side components called identity authenticators. Stock identity authenticators include:

  • Username/password login
  • One-time password verification to an email address or telephone number
  • Time-based one-time password verification using an authenticator app, such as Google Authenticator or Authy
  • reCAPTCHA

The Authentication API and the Auth UI also handle other events that may occur during the authentication flow, such as authorization of scopes, user registration, password change, and password recovery. By customizing the server configuration, the Auth UI, and the identity authenticators, the Data Governance Broker’s authentication and authorization process can be tailored to your organization’s requirements.

Again, this authentication process happens entirely without your application’s active involvement; all of these concerns are separated from your client application, which need only deal with the relatively simple OAuth 2 or OpenID Connect interfaces.