/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * docs/licenses/cddl.txt * or http://www.opensource.org/licenses/cddl1.php. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * docs/licenses/cddl.txt. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2012-2016 UnboundID Corp. */ package com.unboundid.directory.sdk.examples; import com.unboundid.directory.sdk.http.api.OAuthTokenHandler; import com.unboundid.directory.sdk.http.config.OAuthTokenHandlerConfig; import com.unboundid.directory.sdk.http.types.HTTPServerContext; import com.unboundid.ldap.sdk.DN; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.Filter; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.ldap.sdk.SearchScope; import com.unboundid.scim.sdk.DeleteResourceRequest; import com.unboundid.scim.sdk.GetResourceRequest; import com.unboundid.scim.sdk.GetResourcesRequest; import com.unboundid.scim.sdk.OAuthToken; import com.unboundid.scim.sdk.OAuthTokenStatus; import com.unboundid.scim.sdk.PatchResourceRequest; import com.unboundid.scim.sdk.PostResourceRequest; import com.unboundid.scim.sdk.PutResourceRequest; import com.unboundid.scim.sdk.SCIMRequest; import com.unboundid.util.Base64; import com.unboundid.util.StaticUtils; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.DNArgument; import com.unboundid.util.args.FileArgument; import java.io.File; import java.io.RandomAccessFile; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Signature; import java.security.spec.X509EncodedKeySpec; import java.text.ParseException; import java.util.Arrays; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; /** * This class provides a simple implementation of a OAuth Token Handler * that validates access tokens from an authorization server encoded in the * following format: *
* bearer_token := * [expiration_date][audience][scope][auth_user_id][signature] * * expiration_date := * encoded timestamp representing milliseconds since midnight, January 1, * 1970 UTC. (8 bytes) * * audience := * server UUID of the target resource server (16 bytes) * * scope := * an 16-bit (2 byte) mask, starting with the most significant bit: * * bit1: allow GET operations * bit2: allow SEARCH operations * bit3: allow POST operations * bit4: allow PUT operations * bit5: allow PATCH operations * bit6: allow DELETE operations * bits7-16: unused * * auth_user_id := * the entryUUID of the authorization entry for which to apply access * controls (16 bytes) * * signature := * an RSA-SHA1 signature of the * [expiration_date][audience][scope][auth_user_id], using the * authorization server's 1024-bit private RSA key (128 bytes) ** * Note this is a simplistic token encoding and is used only for the purpose of * demonstration. Production implementations should be sure to follow the * guidelines in the "OAuth 2.0 Threat Model and Security Considerations" * (RFC 6819). *
* Also note that the OAuth "scope" is implementation-defined, and thus needs * to be evaluated and applied within this extension, whereas standard LDAP * access controls will be applied automatically within the Directory Server * using the authorization entity (auth_user_id) from the token. This will * happen after the access token is validated using this extension. *
* * This example takes two configuration arguments: *
* Implementers may choose to return a subclass of {@link OAuthToken} in order * to provide convenience methods for interacting with the token. This can be * helpful because the returned {@link OAuthToken} is passed to all of the * other methods in this class. * * @param rawTokenValue the b64token token value. Note that b64token is just * an ABNF syntax definition and does not imply any * base64-encoding of the token value. However, this * OAuthTokenHandler example does expect the raw token * value to be base64-encoded, because it uses a * concatenation of raw bytes to make up the token. * @return a {@link OAuthToken} instance. This must not be {@code null}. * @throws GeneralSecurityException if there is an error decoding the token */ @Override public OAuthToken decodeOAuthToken(final String rawTokenValue) throws GeneralSecurityException { try { //Use a special subclass of OAuthToken which understands base64-encoding. return new Base64OAuthToken(rawTokenValue); } catch (ParseException e) { throw new GeneralSecurityException(StaticUtils.getExceptionMessage(e)); } } /** * Determines whether the given token is expired. * * @param token the OAuth 2.0 bearer token. * @return {@code true} if the token is already expired, {@code false} if not. * @throws GeneralSecurityException if there is an error determining the * token's expiration date */ @Override public boolean isTokenExpired(final OAuthToken token) throws GeneralSecurityException { Base64OAuthToken b64Token = (Base64OAuthToken) token; //Extract the first 8 bytes from the raw token byte[] dBytes = new byte[8]; System.arraycopy(b64Token.getRawTokenBytes(), 0, dBytes, 0, dBytes.length); Date expirationDate = new Date(bytesToLong(dBytes)); Date now = new Date(); return expirationDate.before(now); } /** * Determines whether the incoming token is authentic (i.e. that it came from * a trusted authorization server and not an attacker). Implementers are * encouraged to use signed tokens and use this method to verify the * signature, but other methods such as symmetric key encryption (using a * shared secret) can be used as well. * * @param token the OAuth 2.0 bearer token. * @return {@code true} if the bearer token can be verified as authentic and * originating from a trusted source, {@code false} if not. * @throws java.security.GeneralSecurityException * if there is an error determining whether the token is authentic */ @Override public boolean isTokenAuthentic(final OAuthToken token) throws GeneralSecurityException { byte[] tokenBytes = new byte[42]; byte[] signatureBytes = new byte[128]; Base64OAuthToken b64Token = (Base64OAuthToken) token; //Extract the token bytes and the signature bytes from the raw token System.arraycopy(b64Token.getRawTokenBytes(), 0, tokenBytes, 0, tokenBytes.length); System.arraycopy(b64Token.getRawTokenBytes(), tokenBytes.length, signatureBytes, 0, signatureBytes.length); //Synchronize here to make this code thread-safe, since it may be called //simultaneously by multiple threads and is using the shared Signature //object synchronized (this) { signature.update(tokenBytes); return signature.verify(signatureBytes); } } /** * Determines whether the incoming token is targeted for this server. This * allows the implementation to reject the token early in the validation * process if it can see that the intended recipient was not this server. * * @param token the OAuth 2.0 bearer token. * @return {@code true} if the bearer token identifies this server as the * intended recipient, {@code false} if not. * @throws java.security.GeneralSecurityException * if there is an error determining whether the token is for this * server */ @Override public boolean isTokenForThisServer(final OAuthToken token) throws GeneralSecurityException { byte[] mostSignificantBits = new byte[8]; byte[] leastSignificantBits = new byte[8]; Base64OAuthToken b64Token = (Base64OAuthToken) token; //Extract the serverUUID bytes from the raw token System.arraycopy(b64Token.getRawTokenBytes(), 8, mostSignificantBits, 0, mostSignificantBits.length); System.arraycopy(b64Token.getRawTokenBytes(), 16, leastSignificantBits, 0, leastSignificantBits.length); UUID targetServerUUID = new UUID(bytesToLong(mostSignificantBits), bytesToLong(leastSignificantBits)); return targetServerUUID.equals(serverUUID); } /** * Determines whether the incoming token is valid for the given request. This * method should verify that the token is legitimate and grants access to the * requested resource specified in the {@link SCIMRequest}. This typically * involves checking the token scope and any other attributes granted by the * authorization grant. Implementations may need to call back to the * authorization server to verify that the token is still valid and has not * been revoked. * * @param token the OAuth 2.0 bearer token. * @param scimRequest the {@link SCIMRequest} that we are validating. * @return an {@link OAuthTokenStatus} object which indicates whether the * bearer token is valid and grants access to the target resource. * This must not be {@code null}. * @throws GeneralSecurityException if there is an error determining whether * the token is valid */ @Override public OAuthTokenStatus validateToken(final OAuthToken token, final SCIMRequest scimRequest) throws GeneralSecurityException { //There are 2 bytes allocated to the scope, but we'll extract them into an //array of 4 bytes and convert that to an integer so we can perform bit-wise //arithmetic to check the scope values. byte[] scopeBytes = new byte[4]; Base64OAuthToken b64Token = (Base64OAuthToken) token; //Extract the OAuth scope from the raw token System.arraycopy(b64Token.getRawTokenBytes(), 24, scopeBytes, 0, 2); int scope = 0; for (int i = 0; i < scopeBytes.length; i++) { scope <<= 8; scope |= (scopeBytes[i] & 0xFF); } if (scimRequest instanceof GetResourceRequest) { return checkScope(scope, ALLOW_GET); } else if (scimRequest instanceof GetResourcesRequest) { return checkScope(scope, ALLOW_SEARCH); } else if (scimRequest instanceof PostResourceRequest) { return checkScope(scope, ALLOW_POST); } else if (scimRequest instanceof PutResourceRequest) { return checkScope(scope, ALLOW_PUT); } else if (scimRequest instanceof PatchResourceRequest) { return checkScope(scope, ALLOW_PATCH); } else if (scimRequest instanceof DeleteResourceRequest) { return checkScope(scope, ALLOW_DELETE); } else { return new OAuthTokenStatus(OAuthTokenStatus.ErrorCode.INVALID_TOKEN, "Could not determine request type"); } } /** * Extracts the DN of the authorization entry (for which to apply access * controls) from the incoming token. *
* This may require performing an LDAP search in order to find the DN that * matches a certain attribute value contained in the token. * * @param token the OAuth 2.0 bearer token. * @return the authorization DN to use. This must not return {@code null}. * @throws java.security.GeneralSecurityException * if there is an error determining the authorization user DN */ @Override public DN getAuthzDN(final OAuthToken token) throws GeneralSecurityException { byte[] mostSignificantBits = new byte[8]; byte[] leastSignificantBits = new byte[8]; Base64OAuthToken b64Token = (Base64OAuthToken) token; //Extract the entryUUID bytes from the raw token System.arraycopy(b64Token.getRawTokenBytes(), 26, mostSignificantBits, 0, mostSignificantBits.length); System.arraycopy(b64Token.getRawTokenBytes(), 34, leastSignificantBits, 0, leastSignificantBits.length); UUID authzEntryUUID = new UUID(bytesToLong(mostSignificantBits), bytesToLong(leastSignificantBits)); try { Entry entry = serverContext.getInternalRootConnection().searchForEntry( authorizationUserBaseDN.toString(), SearchScope.SUB, Filter.createEqualityFilter("entryUUID", authzEntryUUID.toString())); if (entry != null) { return entry.getParsedDN(); } else { throw new LDAPException(ResultCode.NO_SUCH_OBJECT); } } catch (LDAPException e) { throw new GeneralSecurityException( "Could not find the authorization entry", e); } } /** * Helper to verify that the given scope has the privileges specified by the * given bit mask. * * @param scope the OAuth scope bytes from the access token. * @param bitMask the scope bytes to check against. * @return an {@link OAuthTokenStatus} instance indicating whether the given * token scope allows the access required to perform the requested * operation. */ private OAuthTokenStatus checkScope(final int scope, final int bitMask) { if ((scope & bitMask) != 0) { return new OAuthTokenStatus( OAuthTokenStatus.ErrorCode.OK); } else { return new OAuthTokenStatus( OAuthTokenStatus.ErrorCode.INSUFFICIENT_SCOPE); } } /** * Helper to expand a compact 8-byte array to a Java long object. * * @param bytes the byte array to convert. * @return a Java long object. * @throws IllegalArgumentException if the byte array does not contain exactly * 8 bytes. */ private long bytesToLong(final byte[] bytes) throws IllegalArgumentException { if (bytes == null || bytes.length != 8) { throw new IllegalArgumentException( "The byte array did not contain exactly 8 bytes"); } long l = 0L; for (int i = 0; i < bytes.length; i++) { l <<= 8; l |= (bytes[i] & 0xFF); } return l; } /** * An OAuthToken implementation for working with base64-encoded token values. */ private static class Base64OAuthToken extends OAuthToken { //The base64-decoded token bytes. private byte[] rawTokenBytes; /** * Constructs an OAuth 2.0 bearer token with the given base64-encoded value. * * @param tokenValue The bearer token value. * @throws ParseException if the token value cannot be parsed as a base64 * encoded string. */ Base64OAuthToken(final String tokenValue) throws ParseException { super(tokenValue); rawTokenBytes = Base64.decode(tokenValue); } /** * Gets the raw token bytes. * * @return a byte array. */ final byte[] getRawTokenBytes() { return rawTokenBytes; } } }