From f098b470ee98ecd478845cd0442a141e6a9b3a54 Mon Sep 17 00:00:00 2001
From: Yannick Lecaillez <yannick.lecaillez@forgerock.com>
Date: Wed, 22 Jun 2016 10:02:34 +0000
Subject: [PATCH] OPENDJ-3015: Oauth2 HTTP client must support HTTPS
---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java | 70 ++--
opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties | 2
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json | 669 +++++++++++++++++++++++++++----------------
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java | 9
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java | 105 ++++++
5 files changed, 569 insertions(+), 286 deletions(-)
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
index 460d55b..5b16ad3 100644
--- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
+++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
@@ -1,9 +1,47 @@
{
+ "security": {
+ // Specifies the policy for trusting server certificates exchanged
+ // during SSL/StartTLS negotiation. This setting and the following
+ // trust policy settings will be ignored if there is no connection
+ // security. Acceptable values are:
+ //
+ // "trustAll" - blindly trust all server certificates
+ // "jvm" - only certificates signed by the authorities
+ // associated with the host JVM will be accepted (default)
+ // "file" - use a file-based trust store for validating
+ // certificates. This option requires the following
+ // "fileBasedTrustManager*" settings to be configured.
+ //
+ "trustManager": "jvm",
+
+ // File based trust manager configuration (see above).
+ "fileBasedTrustManagerType": "JKS",
+ "fileBasedTrustManagerFile": "/path/to/truststore",
+ "fileBasedTrustManagerPasswordFile": "/path/to/pinfile",
+
+ // Specifies the key-manager for authenticating the http client performing
+ // the request against the access-token resolver endpoint. Acceptable values are:
+ //
+ // "jvm" - use the JVM's default keystore for retrieving certificates. (default)
+ // "keystore" - use the named key store file for retrieving certificates.
+ // "pkcs11" - use a PKCS#11 token for retrieving certificates.
+ "keyManager": "jvm",
+
+ // Keystore based key manager configuration (see above).
+ "keyStoreFile": "/path/to/keystore",
+ "keyStorePasswordFile": "/path/to/pinfile",
+ "keyStoreFormat": "JKS",
+ "keyStoreProvider": "",
+
+ // PKCS11 based key manager configuration
+ "pkcs11PasswordFile": "/path/to/pinfile"
+ },
+
// The array of connection factories which will be used by the Rest2LDAP
// Servlet and authentication filter.
- "ldapConnectionFactories" : {
+ "ldapConnectionFactories": {
// Unauthenticated connections used for performing bind requests.
- "bind" : {
+ "bind": {
// Indicates whether LDAP connections should be secured using
// SSL or StartTLS. Acceptable values are:
//
@@ -11,44 +49,27 @@
// "ssl" - secure connection using LDAPS
// "startTLS" - secure connection using LDAP+StartTLS
//
- "connectionSecurity" : "none",
+ "connectionSecurity": "none",
- // Specifies the policy for trusting server certificates exchanged
- // during SSL/StartTLS negotiation. This setting and the following
- // trust policy settings will be ignored if there is no connection
- // security. Acceptable values are:
- //
- // "trustAll" - blindly trust all server certificates (default)
- // "jvm" - only certificates signed by the authorities
- // associated with the host JVM will be accepted
- // "file" - use a file-based trust store for validating
- // certificates. This option requires the following
- // "fileBasedTrustManager*" settings to be configured.
- //
- "trustManager" : "trustAll",
-
- // File based trust manager configuration (see above).
- "fileBasedTrustManagerType" : "JKS",
- "fileBasedTrustManagerFile" : "/path/to/truststore",
- "fileBasedTrustManagerPassword" : "password",
+ // This alias points at an existing certificate that is used for SSL authentication for secure
+ // communication between this gateway and the remote LDAP server.
+ "sslCertAlias": "client-cert",
// Re-usable pool of 24 connections per server.
- "connectionPoolSize" : 24,
+ "connectionPoolSize": 24,
// Check pooled connections are alive every 30 seconds with a 500ms
// heart beat timeout.
- "heartBeatIntervalSeconds" : 30,
- "heartBeatTimeoutMilliSeconds" : 500,
+ "heartBeatIntervalSeconds": 30,
+ "heartBeatTimeoutMilliSeconds": 500,
// The preferred load-balancing pool.
- "primaryLDAPServers" : [
- {
- "hostname" : "localhost",
- "port" : 1389
- }
- ],
+ "primaryLDAPServers": [{
+ "hostname": "localhost",
+ "port": 1389
+ }],
// The fail-over load-balancing pool (optional).
- "secondaryLDAPServers" : [
+ "secondaryLDAPServers": [
// Empty.
]
},
@@ -56,25 +77,25 @@
// Authenticated connections which will be used for searches during
// authentication and proxied operations (if enabled). This factory
// will re-use the server "bind" configuration.
- "root" : {
- "inheritFrom" : "bind",
+ "root": {
+ "inheritFrom": "bind",
// Defines how authentication should be performed. Only "simple"
// authentication is supported at the moment.
// If the OAuth 2.0 authorization policy is configured below,
// then the directory service must be configured
// to allow the user configured here to perform proxied authorization.
- "authentication" : {
- "simple" : {
- "bindDN" : "cn=directory manager",
- "bindPassword" : "password"
+ "authentication": {
+ "simple": {
+ "bindDN": "cn=directory manager",
+ "bindPassword": "password"
}
}
}
},
"authorization": {
- // The authorization policies to use. Supported policies are "anonymous", "basic" and "oauth2".
+ // The authorization policies to use. Supported policies are "anonymous", "basic" and "oauth2".
"policies": [ "basic" ],
// Perform all operations using a pre-authorization connection.
@@ -84,77 +105,77 @@
"ldapConnectionFactory": "root"
},
- // Use HTTP Basic authentication's information to bind to the LDAP server.
- "basic": {
- // Indicates whether the filter should allow alternative authentication
- // and, if so, which HTTP headers it should obtain the username and
- // password from.
- "supportAltAuthentication" : true,
- "altAuthenticationUsernameHeader" : "X-OpenIDM-Username",
- "altAuthenticationPasswordHeader" : "X-OpenIDM-Password",
+ // Use HTTP Basic authentication's information to bind to the LDAP server.
+ "basic": {
+ // Indicates whether the filter should allow alternative authentication
+ // and, if so, which HTTP headers it should obtain the username and
+ // password from.
+ "supportAltAuthentication": true,
+ "altAuthenticationUsernameHeader": "X-OpenIDM-Username",
+ "altAuthenticationPasswordHeader": "X-OpenIDM-Password",
- // Define which LDAP bind mechanism to use
- // Supported mechanisms are "simple", "sasl-plain", "search"
- "bind": "search",
+ // Define which LDAP bind mechanism to use
+ // Supported mechanisms are "simple", "sasl-plain", "search"
+ "bind": "search",
- // Bind to the LDAP server using the DN built from the HTTP Basic's username
- "simple": {
- // Connection factory used to perform the bind operation.
- // If missing, "bind" factory will be used.
- "ldapConnectionFactory": "bind",
+ // Bind to the LDAP server using the DN built from the HTTP Basic's username
+ "simple": {
+ // Connection factory used to perform the bind operation.
+ // If missing, "bind" factory will be used.
+ "ldapConnectionFactory": "bind",
- // The Bind DN Template containing a single {username} which will be replaced by the authenticating
- // user's name. (i.e: uid={username},ou=People,dc=example,dc=com)
- // If missing, "{username}" is used.
- "bindDNTemplate": "uid={username},ou=People,dc=example,dc=com"
- },
+ // The Bind DN Template containing a single {username} which will be replaced by the authenticating
+ // user's name. (i.e: uid={username},ou=People,dc=example,dc=com)
+ // If missing, "{username}" is used.
+ "bindDNTemplate": "uid={username},ou=People,dc=example,dc=com"
+ },
- // Bind to the LDAP server using a SASL Plain request
- "sasl-plain": {
- // Connection factory used to perform the bind operation.
- // If missing, "bind" factory will be used.
- "ldapConnectionFactory": "bind",
+ // Bind to the LDAP server using a SASL Plain request
+ "sasl-plain": {
+ // Connection factory used to perform the bind operation.
+ // If missing, "bind" factory will be used.
+ "ldapConnectionFactory": "bind",
- // Authentication identity template containing a single {username} which will be replaced by the authenticating
+ // Authentication identity template containing a single {username} which will be replaced by the authenticating
// user's name. (i.e: u:{username})
- "authzIdTemplate": "u:{username}"
- },
-
- // Bind to the LDAP server using the resulting DN of a search request.
- "search": {
- // Connection factory used to perform the search operation.
- // If missing, "root" factory will be used.
- "searchLDAPConnectionFactory": "root",
-
- // Connection factory used to perform the bind operation.
- // If missing, "bind" factory will be used.
- "bindLDAPConnectionFactory": "bind",
-
+ "authzIdTemplate": "u:{username}"
+ },
+
+ // Bind to the LDAP server using the resulting DN of a search request.
+ "search": {
+ // Connection factory used to perform the search operation.
+ // If missing, "root" factory will be used.
+ "searchLDAPConnectionFactory": "root",
+
+ // Connection factory used to perform the bind operation.
+ // If missing, "bind" factory will be used.
+ "bindLDAPConnectionFactory": "bind",
+
// The {username} filter format parameters will be substituted with the client-provided username,
// using LDAP filter string character escaping.
- "baseDN" : "ou=people,dc=example,dc=com",
- "scope" : "sub", // Or "one".
- "filterTemplate" : "(&(uid={username})(objectClass=inetOrgPerson))"
- }
- // TODO: support for HTTP sessions?
- },
+ "baseDN": "ou=people,dc=example,dc=com",
+ "scope": "sub", // Or "one".
+ "filterTemplate": "(&(uid={username})(objectClass=inetOrgPerson))"
+ }
+ // TODO: support for HTTP sessions?
+ },
// Use OAuth2 authorization method. If used, LDAP requests will be performed with proxied authorization control.
// This field is optional.
"oauth2": {
- // Access tokens associated realm.
- // This attribute is optional and has a string syntax.
- "realm": "myrealm",
+ // Access tokens associated realm.
+ // This attribute is optional and has a string syntax.
+ "realm": "myrealm",
// Defines the list of required scopes required to access the service.
// This field is required and cannot be empty.
"requiredScopes": [ "read", "write", "uid" ],
- // Specify the resolver to use to resolve OAuth2 access token.
- // This attribute is required and its value must be one of "openam", "rfc7662", "cts".
- // Note that the JSON object corresponding to this attribute value must be present
- // and well formed in the "oauth2" JSON attribute.
- "resolver": "openam",
+ // Specify the resolver to use to resolve OAuth2 access token.
+ // This attribute is required and its value must be one of "openam", "rfc7662", "cts".
+ // Note that the JSON object corresponding to this attribute value must be present
+ // and well formed in the "oauth2" JSON attribute.
+ "resolver": "openam",
// Configures caching of access token introspection results.
// This attribute is optional, if it is not present, no token caching will be performed.
@@ -180,17 +201,21 @@
// This attribute is required and must have a string syntax.
"endpointURL": "http://openam.example.com:8080/openam/oauth2/tokeninfo",
- // The default authzIdTemplate demonstrates how an authorization DN may be constructed
- // from the "uid" field in the following example OpenAM tokeninfo response:
- // {
- // "scope":["uid"],
- // "realm":"/",
- // "expires_in":45,
- // "uid" : "bjensen",
- // }
- // This attribute is required and has a string syntax.
- // It must start with either 'dn:' or 'u:'.
- "authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com"
+ // This alias points at an existing certificate that is used for SSL authentication for secure
+ // communication between this gateway and the OpenAM access-token resolver.
+ "sslCertAlias": "client-cert",
+
+ // The default authzIdTemplate demonstrates how an authorization DN may be constructed
+ // from the "uid" field in the following example OpenAM tokeninfo response:
+ // {
+ // "scope":["uid"],
+ // "realm":"/",
+ // "expires_in":45,
+ // "uid" : "bjensen",
+ // }
+ // This attribute is required and has a string syntax.
+ // It must start with either 'dn:' or 'u:'.
+ "authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com"
},
// The RFC-7662 (see https://tools.ietf.org/html/rfc7662) access token resolver configuration.
@@ -201,52 +226,56 @@
// This attribute is required and must have a string syntax.
"endpointURL": "http://openam.example.com:8080/openam/oauth2/myrealm/introspect",
- // Token introspect endpoint requires authentication.
- // It should support HTTP basic authorization (a base64-encoded string of clientId:clientSecret)
- // These attributes are mandatory.
- "clientId": "client_id",
- "clientSecret": "client_secret",
+ // This alias points at an existing certificate that is used for SSL authentication for secure
+ // communication between this gateway and the introspection access-token resolver.
+ "sslCertAlias": "client-cert",
- // The default authzIdTemplate demonstrates how an authorization DN may be constructed
- // from the "username" field in the following example introspect response:
- // {
- // "active": true,
- // "token_type": "access_token",
- // "exp": 3524,
- // "username" : "bjensen",
- // }
- // This attribute is required and has a string syntax.
- // It must start with either 'dn:' or 'u:'.
- "authzIdTemplate": "dn:uid={username},ou=People,dc=example,dc=com"
+ // Token introspect endpoint requires authentication.
+ // It should support HTTP basic authorization (a base64-encoded string of clientId:clientSecret)
+ // These attributes are mandatory.
+ "clientId": "client_id",
+ "clientSecret": "client_secret",
+
+ // The default authzIdTemplate demonstrates how an authorization DN may be constructed
+ // from the "username" field in the following example introspect response:
+ // {
+ // "active": true,
+ // "token_type": "access_token",
+ // "exp": 3524,
+ // "username" : "bjensen",
+ // }
+ // This attribute is required and has a string syntax.
+ // It must start with either 'dn:' or 'u:'.
+ "authzIdTemplate": "dn:uid={username},ou=People,dc=example,dc=com"
},
// The CTS access token resolver.
// This attribute must be present if the "oauth2/resolver" is equal to "cts".
// If "oauth2/resolver" is set to another resolver, this attribute will be ignored.
- // Note: You can use {userName/0} in authzIdTemplate configuration to access
- // user id from the default CTS access token content config.
+ // Note: You can use {userName/0} in authzIdTemplate configuration to access
+ // user id from the default CTS access token content config.
"cts": {
- // The connection factory to use to access CTS.
- // This value is only used in gateway mode.
- // This attribute must reference a connection factory defined in the "ldapConnectionFactories" section.
- // Default value: "root" (i.e the root connection factory will be used to access the CTS).
- "ldapConnectionFactory": "root",
+ // The connection factory to use to access CTS.
+ // This value is only used in gateway mode.
+ // This attribute must reference a connection factory defined in the "ldapConnectionFactories" section.
+ // Default value: "root" (i.e the root connection factory will be used to access the CTS).
+ "ldapConnectionFactory": "root",
// The access token base DN.
// This attribute is required and must have a string syntax.
"baseDN": "ou=famrecords,ou=openam-session,ou=tokens,dc=example,dc=com",
- // The default authzIdTemplate demonstrates how an authorization DN may be constructed
- // from the "userName" field in the following example CTS access token entry:
- // {
- // "active": true,
- // "tokenName": ["access_token"],
- // "exp": [3524],
- // "userName" : ["bjensen"],
- // }
- // This attribute is required and has a string syntax.
- // It must start with either 'dn:' or 'u:'.
- "authzIdTemplate": "dn:uid={userName/0},ou=People,dc=example,dc=com"
+ // The default authzIdTemplate demonstrates how an authorization DN may be constructed
+ // from the "userName" field in the following example CTS access token entry:
+ // {
+ // "active": true,
+ // "tokenName": ["access_token"],
+ // "exp": [3524],
+ // "userName" : ["bjensen"],
+ // }
+ // This attribute is required and has a string syntax.
+ // It must start with either 'dn:' or 'u:'.
+ "authzIdTemplate": "dn:uid={userName/0},ou=People,dc=example,dc=com"
},
// ONLY FOR TEST PURPOSE: A File based access token resolver
@@ -257,125 +286,263 @@
// You can test the rest2ldap OAuth2 authorization support by providing some json token files under
// the directory set in the configuration below.
// File names must be equal to the token strings.
- // The file content must a JSON object with the following attributes:
- // 'scope', 'expireTime' and all the field(s) needed to resolve the authzIdTemplate.
+ // The file content must a JSON object with the following attributes:
+ // 'scope', 'expireTime' and all the field(s) needed to resolve the authzIdTemplate.
"folderPath": "/path/to/test/folder",
- // The default authzIdTemplate demonstrates how an authorization DN may be constructed
- // from the "uid" field extracted from a fake token file:
- // {
- // "scope": ["read", "uid", "write"],
- // "expireTime": 1961336698000,
- // "uid": "bjensen"
- // }
- // This attribute is required and has a string syntax.
- // It must start with either 'dn:' or 'u:'.
- "authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com"
+ // The default authzIdTemplate demonstrates how an authorization DN may be constructed
+ // from the "uid" field extracted from a fake token file:
+ // {
+ // "scope": ["read", "uid", "write"],
+ // "expireTime": 1961336698000,
+ // "uid": "bjensen"
+ // }
+ // This attribute is required and has a string syntax.
+ // It must start with either 'dn:' or 'u:'.
+ "authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com"
}
}
},
- // The REST APIs and their LDAP attribute mappings.
- "mappings" : {
- "/users" : {
- "baseDN" : "ou=people,dc=example,dc=com",
- "readOnUpdatePolicy" : "controls",
- "useSubtreeDelete" : false,
- "usePermissiveModify" : true,
- "etagAttribute" : "etag",
- "namingStrategy" : {
- "strategy" : "clientDNNaming",
- "dnAttribute" : "uid"
- },
- "additionalLDAPAttributes" : [
- {
- "type" : "objectClass",
- "values" : [
- "top",
- "person",
- "organizationalPerson",
- "inetOrgPerson"
- ]
- }
- ],
- "attributes" : {
- "schemas" : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
- "_id" : { "simple" : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
- "_rev" : { "simple" : { "ldapAttribute" : "etag", "isSingleValued" : true, "writability" : "readOnly" } },
- "userName" : { "simple" : { "ldapAttribute" : "mail", "isSingleValued" : true, "writability" : "readOnly" } },
- "displayName" : { "simple" : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true } },
- "name" : { "object" : {
- "givenName" : { "simple" : { "ldapAttribute" : "givenName", "isSingleValued" : true } },
- "familyName" : { "simple" : { "ldapAttribute" : "sn", "isSingleValued" : true, "isRequired" : true } }
- } },
- "manager" : { "reference" : {
- "ldapAttribute" : "manager",
- "baseDN" : "ou=people,dc=example,dc=com",
- "primaryKey" : "uid",
- "mapper" : { "object" : {
- "_id" : { "simple" : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
- "displayName" : { "simple" : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
- } }
- } },
- "groups" : { "reference" : {
- "ldapAttribute" : "isMemberOf",
- "baseDN" : "ou=groups,dc=example,dc=com",
- "writability" : "readOnly",
- "primaryKey" : "cn",
- "mapper" : { "object" : {
- "_id" : { "simple" : { "ldapAttribute" : "cn", "isSingleValued" : true } }
- } }
- } },
- "contactInformation" : { "object" : {
- "telephoneNumber" : { "simple" : { "ldapAttribute" : "telephoneNumber", "isSingleValued" : true } },
- "emailAddress" : { "simple" : { "ldapAttribute" : "mail", "isSingleValued" : true } }
- } },
- "meta" : { "object" : {
- "created" : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
- "lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
- } }
- }
- },
- "/groups" : {
- "baseDN" : "ou=groups,dc=example,dc=com",
- "readOnUpdatePolicy" : "controls",
- "useSubtreeDelete" : false,
- "usePermissiveModify" : true,
- "etagAttribute" : "etag",
- "namingStrategy" : {
- "strategy" : "clientDNNaming",
- "dnAttribute" : "cn"
- },
- "additionalLDAPAttributes" : [
- {
- "type" : "objectClass",
- "values" : [
- "top",
- "groupOfUniqueNames"
- ]
- }
- ],
- "attributes" : {
- "schemas" : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
- "_id" : { "simple" : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
- "_rev" : { "simple" : { "ldapAttribute" : "etag", "isSingleValued" : true, "writability" : "readOnly" } },
- "displayName" : { "simple" : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "readOnly" } },
- "members" : { "reference" : {
- "ldapAttribute" : "uniqueMember",
- "baseDN" : "dc=example,dc=com",
- "primaryKey" : "uid",
- "mapper" : { "object" : {
- "_id" : { "simple" : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
- "displayName" : { "simple" : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
- } }
- } },
- "meta" : { "object" : {
- "created" : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
- "lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
- } }
- }
- }
- }
+ // The REST APIs and their LDAP attribute mappings.
+ "mappings": {
+ "/users": {
+ "baseDN": "ou=people,dc=example,dc=com",
+ "readOnUpdatePolicy": "controls",
+ "useSubtreeDelete": false,
+ "usePermissiveModify": true,
+ "etagAttribute": "etag",
+ "namingStrategy": {
+ "strategy": "clientDNNaming",
+ "dnAttribute": "uid"
+ },
+ "additionalLDAPAttributes": [{
+ "type": "objectClass",
+ "values": [
+ "top",
+ "person",
+ "organizationalPerson",
+ "inetOrgPerson"
+ ]
+ }],
+ "attributes": {
+ "schemas": {
+ "constant": ["urn:scim:schemas:core:1.0"]
+ },
+ "_id": {
+ "simple": {
+ "ldapAttribute": "uid",
+ "isSingleValued": true,
+ "isRequired": true,
+ "writability": "createOnly"
+ }
+ },
+ "_rev": {
+ "simple": {
+ "ldapAttribute": "etag",
+ "isSingleValued": true,
+ "writability": "readOnly"
+ }
+ },
+ "userName": {
+ "simple": {
+ "ldapAttribute": "mail",
+ "isSingleValued": true,
+ "writability": "readOnly"
+ }
+ },
+ "displayName": {
+ "simple": {
+ "ldapAttribute": "cn",
+ "isSingleValued": true,
+ "isRequired": true
+ }
+ },
+ "name": {
+ "object": {
+ "givenName": {
+ "simple": {
+ "ldapAttribute": "givenName",
+ "isSingleValued": true
+ }
+ },
+ "familyName": {
+ "simple": {
+ "ldapAttribute": "sn",
+ "isSingleValued": true,
+ "isRequired": true
+ }
+ }
+ }
+ },
+ "manager": {
+ "reference": {
+ "ldapAttribute": "manager",
+ "baseDN": "ou=people,dc=example,dc=com",
+ "primaryKey": "uid",
+ "mapper": {
+ "object": {
+ "_id": {
+ "simple": {
+ "ldapAttribute": "uid",
+ "isSingleValued": true,
+ "isRequired": true
+ }
+ },
+ "displayName": {
+ "simple": {
+ "ldapAttribute": "cn",
+ "isSingleValued": true,
+ "writability": "readOnlyDiscardWrites"
+ }
+ }
+ }
+ }
+ }
+ },
+ "groups": {
+ "reference": {
+ "ldapAttribute": "isMemberOf",
+ "baseDN": "ou=groups,dc=example,dc=com",
+ "writability": "readOnly",
+ "primaryKey": "cn",
+ "mapper": {
+ "object": {
+ "_id": {
+ "simple": {
+ "ldapAttribute": "cn",
+ "isSingleValued": true
+ }
+ }
+ }
+ }
+ }
+ },
+ "contactInformation": {
+ "object": {
+ "telephoneNumber": {
+ "simple": {
+ "ldapAttribute": "telephoneNumber",
+ "isSingleValued": true
+ }
+ },
+ "emailAddress": {
+ "simple": {
+ "ldapAttribute": "mail",
+ "isSingleValued": true
+ }
+ }
+ }
+ },
+ "meta": {
+ "object": {
+ "created": {
+ "simple": {
+ "ldapAttribute": "createTimestamp",
+ "isSingleValued": true,
+ "writability": "readOnly"
+ }
+ },
+ "lastModified": {
+ "simple": {
+ "ldapAttribute": "modifyTimestamp",
+ "isSingleValued": true,
+ "writability": "readOnly"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/groups": {
+ "baseDN": "ou=groups,dc=example,dc=com",
+ "readOnUpdatePolicy": "controls",
+ "useSubtreeDelete": false,
+ "usePermissiveModify": true,
+ "etagAttribute": "etag",
+ "namingStrategy": {
+ "strategy": "clientDNNaming",
+ "dnAttribute": "cn"
+ },
+ "additionalLDAPAttributes": [{
+ "type": "objectClass",
+ "values": [
+ "top",
+ "groupOfUniqueNames"
+ ]
+ }],
+ "attributes": {
+ "schemas": {
+ "constant": ["urn:scim:schemas:core:1.0"]
+ },
+ "_id": {
+ "simple": {
+ "ldapAttribute": "cn",
+ "isSingleValued": true,
+ "isRequired": true,
+ "writability": "createOnly"
+ }
+ },
+ "_rev": {
+ "simple": {
+ "ldapAttribute": "etag",
+ "isSingleValued": true,
+ "writability": "readOnly"
+ }
+ },
+ "displayName": {
+ "simple": {
+ "ldapAttribute": "cn",
+ "isSingleValued": true,
+ "isRequired": true,
+ "writability": "readOnly"
+ }
+ },
+ "members": {
+ "reference": {
+ "ldapAttribute": "uniqueMember",
+ "baseDN": "dc=example,dc=com",
+ "primaryKey": "uid",
+ "mapper": {
+ "object": {
+ "_id": {
+ "simple": {
+ "ldapAttribute": "uid",
+ "isSingleValued": true,
+ "isRequired": true
+ }
+ },
+ "displayName": {
+ "simple": {
+ "ldapAttribute": "cn",
+ "isSingleValued": true,
+ "writability": "readOnlyDiscardWrites"
+ }
+ }
+ }
+ }
+ }
+ },
+ "meta": {
+ "object": {
+ "created": {
+ "simple": {
+ "ldapAttribute": "createTimestamp",
+ "isSingleValued": true,
+ "writability": "readOnly"
+ }
+ },
+ "lastModified": {
+ "simple": {
+ "ldapAttribute": "modifyTimestamp",
+ "isSingleValued": true,
+ "writability": "readOnly"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
-
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index 4aa085f..967b0b3 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -31,8 +31,8 @@
import static org.forgerock.opendj.rest2ldap.Utils.newLocalizedIllegalArgumentException;
import static org.forgerock.opendj.rest2ldap.Utils.newJsonValueException;
import static org.forgerock.util.time.Duration.*;
+import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate;
-import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -42,6 +42,9 @@
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509KeyManager;
+
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.ResourceException;
@@ -68,7 +71,6 @@
import org.forgerock.opendj.ldap.SSLContextBuilder;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.TimeoutResultException;
-import org.forgerock.opendj.ldap.TrustManagers;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
@@ -88,10 +90,18 @@
* Specifies the mechanism which should be used for trusting certificates
* presented by the LDAP server.
*/
- private enum TrustManagerType {
+ enum TrustManagerType {
TRUSTALL, JVM, FILE
}
+ /**
+ * Specifies the mechanism which manage which X509 certificate-based key pairs should be used to authenticate the
+ * local side of a secure socket.
+ */
+ enum KeyManagerType {
+ JVM, KEYSTORE, PKCS11
+ }
+
/** A builder for incrementally constructing LDAP resource collections. */
public static final class Builder {
private final List<Attribute> additionalLDAPAttributes = new LinkedList<>();
@@ -784,6 +794,12 @@
* The JSON configuration.
* @param name
* The name of the connection factory configuration to be parsed.
+ * @param trustManager
+ * The trust manager to use for secure connection. Can be {@code null}
+ * to use the default JVM trust manager.
+ * @param keyManager
+ * The key manager to use for secure connection. Can be {@code null}
+ * to use the default JVM key manager.
* @param providerClassLoader
* The {@link ClassLoader} used to fetch the
* {@link org.forgerock.opendj.ldap.spi.TransportProvider}.
@@ -794,10 +810,12 @@
*/
public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
final String name,
+ final TrustManager trustManager,
+ final X509KeyManager keyManager,
final ClassLoader providerClassLoader) {
final JsonValue normalizedConfiguration =
normalizeConnectionFactory(configuration, name, 0);
- return configureConnectionFactory(normalizedConfiguration, providerClassLoader);
+ return configureConnectionFactory(normalizedConfiguration, trustManager, keyManager, providerClassLoader);
}
/**
@@ -809,13 +827,19 @@
* The JSON configuration.
* @param name
* The name of the connection factory configuration to be parsed.
+ * @param trustManager
+ * The trust manager to use for secure connection. Can be {@code null}
+ * to use the default JVM trust manager.
+ * @param keyManager
+ * The key manager to use for secure connection. Can be {@code null}
+ * to use the default JVM key manager.
* @return A new connection factory using the provided JSON configuration.
* @throws IllegalArgumentException
* If the configuration is invalid.
*/
public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
- final String name) {
- return configureConnectionFactory(configuration, name, null);
+ final String name, final TrustManager trustManager, final X509KeyManager keyManager) {
+ return configureConnectionFactory(configuration, name, trustManager, keyManager, null);
}
/**
@@ -908,6 +932,8 @@
}
private static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
+ final TrustManager trustManager,
+ final X509KeyManager keyManager,
final ClassLoader providerClassLoader) {
final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong();
final Duration heartBeatInterval = duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS);
@@ -948,32 +974,14 @@
try {
// Configure SSL.
final SSLContextBuilder builder = new SSLContextBuilder();
-
- // Parse trust store configuration.
- final TrustManagerType trustManagerType =
- configuration.get("trustManager").defaultTo(TrustManagerType.TRUSTALL)
- .asEnum(TrustManagerType.class);
- switch (trustManagerType) {
- case TRUSTALL:
- builder.setTrustManager(TrustManagers.trustAll());
- break;
- case JVM:
- // Do nothing: JVM trust manager is the default.
- break;
- case FILE:
- final String fileName =
- configuration.get("fileBasedTrustManagerFile").required().asString();
- final String password =
- configuration.get("fileBasedTrustManagerPassword").asString();
- final String type = configuration.get("fileBasedTrustManagerType").asString();
- builder.setTrustManager(TrustManagers.checkUsingTrustStore(fileName,
- password != null ? password.toCharArray() : null, type));
- break;
- }
+ builder.setTrustManager(trustManager);
+ final String sslCertAlias = configuration.get("sslCertAlias").asString();
+ builder.setKeyManager(sslCertAlias != null
+ ? useSingleCertificate(sslCertAlias, keyManager)
+ : keyManager);
options.set(SSL_CONTEXT, builder.getSSLContext());
- options.set(SSL_USE_STARTTLS,
- connectionSecurity == ConnectionSecurity.STARTTLS);
- } catch (GeneralSecurityException | IOException e) {
+ options.set(SSL_USE_STARTTLS, connectionSecurity == ConnectionSecurity.STARTTLS);
+ } catch (GeneralSecurityException e) {
// Rethrow as unchecked exception.
throw new IllegalArgumentException(e);
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
index a59c1a7..5727ba4 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
@@ -17,6 +17,10 @@
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
+import static org.forgerock.http.handler.HttpClientHandler.*;
+import static org.forgerock.opendj.ldap.KeyManagers.*;
+import static org.forgerock.opendj.ldap.TrustManagers.checkUsingTrustStore;
+import static org.forgerock.opendj.ldap.TrustManagers.trustAll;
import static org.forgerock.http.util.Json.readJsonLenient;
import static org.forgerock.json.JsonValueFunctions.duration;
import static org.forgerock.json.JsonValueFunctions.enumConstant;
@@ -31,12 +35,14 @@
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.util.Utils.closeSilently;
import static org.forgerock.util.Utils.joinAsString;
+import static org.forgerock.opendj.rest2ldap.Utils.readPasswordFromFile;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
+import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -46,6 +52,10 @@
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509KeyManager;
+
import org.forgerock.openig.oauth2.AccessTokenInfo;
import org.forgerock.openig.oauth2.AccessTokenException;
import org.forgerock.openig.oauth2.AccessTokenResolver;
@@ -71,11 +81,14 @@
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.rest2ldap.Rest2LDAP.KeyManagerType;
+import org.forgerock.opendj.rest2ldap.Rest2LDAP.TrustManagerType;
import org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategy;
import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.Factory;
import org.forgerock.util.Function;
+import org.forgerock.util.Options;
import org.forgerock.util.Pair;
import org.forgerock.util.PerItemEvictionStrategyCache;
import org.forgerock.util.annotations.VisibleForTesting;
@@ -113,6 +126,9 @@
/** Used for token caching. */
private ScheduledExecutorService executorService;
+ private TrustManager trustManager;
+ private X509KeyManager keyManager;
+
/** Define the method which should be used to resolve an OAuth2 access token. */
private enum OAuth2ResolverType {
RFC7662, OPENAM, CTS, FILE;
@@ -175,6 +191,7 @@
try {
final JsonValue configuration = readJson(configurationUrl);
executorService = Executors.newSingleThreadScheduledExecutor();
+ configureSecurity(configuration.get("security"));
configureConnectionFactories(configuration.get("ldapConnectionFactories"));
return Handlers.chainOf(
CrestHttp.newHttpHandler(configureRest2Ldap(configuration)),
@@ -204,10 +221,76 @@
return router;
}
+ private void configureSecurity(final JsonValue configuration) {
+ try {
+ trustManager = configureTrustManager(configuration, TrustManagerType.JVM);
+ } catch (GeneralSecurityException | IOException e) {
+ throw new IllegalArgumentException(ERR_CONFIG_INVALID_TRUST_MANAGER
+ .get(configuration.getPointer(), e.getLocalizedMessage()).toString(), e);
+ }
+
+ try {
+ keyManager = configureKeyManager(configuration, KeyManagerType.JVM);
+ } catch (GeneralSecurityException | IOException e) {
+ throw new IllegalArgumentException(ERR_CONFIG_INVALID_KEY_MANAGER
+ .get(configuration.getPointer(), e.getLocalizedMessage()).toString(), e);
+ }
+ }
+
+ private TrustManager configureTrustManager(JsonValue config, TrustManagerType defaultIfMissing)
+ throws GeneralSecurityException, IOException {
+ // Parse trust store configuration.
+ final TrustManagerType trustManagerType =
+ config.get("trustManager").defaultTo(defaultIfMissing).as(enumConstant(TrustManagerType.class));
+ switch (trustManagerType) {
+ case TRUSTALL:
+ return trustAll();
+ case JVM:
+ return null;
+ case FILE:
+ final String fileName = config.get("fileBasedTrustManagerFile").required().asString();
+ final String passwordFile = config.get("fileBasedTrustManagerPasswordFile").asString();
+ final String password = passwordFile != null
+ ? readPasswordFromFile(passwordFile)
+ : config.get("fileBasedTrustManagerPassword").asString();
+ final String type = config.get("fileBasedTrustManagerType").asString();
+ return checkUsingTrustStore(fileName, password != null ? password.toCharArray() : null, type);
+ default:
+ throw new IllegalArgumentException("Unsupported trust-manager type: " + trustManagerType);
+ }
+ }
+
+ private X509KeyManager configureKeyManager(JsonValue config, KeyManagerType defaultIfMissing)
+ throws GeneralSecurityException, IOException {
+ // Parse trust store configuration.
+ final KeyManagerType keyManagerType = config.get("keyManager").defaultTo(defaultIfMissing)
+ .as(enumConstant(KeyManagerType.class));
+ switch (keyManagerType) {
+ case JVM:
+ return useJvmDefaultKeyStore();
+ case KEYSTORE:
+ final String fileName = config.get("keyStoreFile").required().asString();
+ final String passwordFile = config.get("keyStorePasswordFile").asString();
+ final String password = passwordFile != null
+ ? readPasswordFromFile(passwordFile)
+ : config.get("keyStorePassword").asString();
+ final String format = config.get("keyStoreFormat").asString();
+ final String provider = config.get("keyStoreProvider").asString();
+ return useKeyStoreFile(fileName, password != null ? password.toCharArray() : null, format, provider);
+ case PKCS11:
+ final String pkcs11PasswordFile = config.get("pkcs11PasswordFile").asString();
+ return usePKCS11Token(pkcs11PasswordFile != null
+ ? readPasswordFromFile(pkcs11PasswordFile).toCharArray()
+ : null);
+ default:
+ throw new IllegalArgumentException("Unsupported key-manager type: " + keyManagerType);
+ }
+ }
+
private void configureConnectionFactories(final JsonValue config) {
connectionFactories.clear();
for (String name : config.keys()) {
- connectionFactories.put(name, configureConnectionFactory(config, name));
+ connectionFactories.put(name, configureConnectionFactory(config, name, trustManager, keyManager));
}
}
@@ -282,9 +365,10 @@
case RFC7662:
return parseRfc7662Resolver(configuration);
case OPENAM:
- return new OpenAmAccessTokenResolver(new HttpClientHandler(),
+ final JsonValue openAm = configuration.get("openam");
+ return new OpenAmAccessTokenResolver(newHttpClientHandler(openAm),
TimeService.SYSTEM,
- configuration.get("openam").get("endpointURL").required().asString());
+ openAm.get("endpointURL").required().asString());
case CTS:
final JsonValue cts = configuration.get("cts").required();
return newCtsAccessTokenResolver(
@@ -303,7 +387,7 @@
final JsonValue rfc7662 = configuration.get("rfc7662").required();
final String introspectionEndPointURL = rfc7662.get("endpointURL").required().asString();
try {
- return newRfc7662AccessTokenResolver(new HttpClientHandler(),
+ return newRfc7662AccessTokenResolver(newHttpClientHandler(rfc7662),
new URI(introspectionEndPointURL),
rfc7662.get("clientId").required().asString(),
rfc7662.get("clientSecret").required().asString());
@@ -313,6 +397,19 @@
}
}
+ private HttpClientHandler newHttpClientHandler(final JsonValue config) throws HttpApplicationException {
+ final Options httpOptions = Options.defaultOptions();
+ if (trustManager != null) {
+ httpOptions.set(OPTION_TRUST_MANAGERS, new TrustManager[] { trustManager });
+ }
+ if (keyManager != null) {
+ final String keyAlias = config.get("sslCertAlias").asString();
+ httpOptions.set(OPTION_KEY_MANAGERS,
+ new KeyManager[] { keyAlias != null ? useSingleCertificate(keyAlias, keyManager) : keyManager });
+ }
+ return new HttpClientHandler(httpOptions);
+ }
+
private Duration parseCacheExpiration(final JsonValue expirationJson) {
try {
final Duration expiration = expirationJson.as(duration());
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
index b67dda7..e7d155c 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -27,6 +27,10 @@
import static org.forgerock.opendj.ldap.schema.CoreSchema.getGeneralizedTimeSyntax;
import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -70,6 +74,11 @@
}
};
+ static String readPasswordFromFile(String fileName) throws IOException {
+ try (final BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)))) {
+ return reader.readLine();
+ }
+ }
static Object attributeToJson(final Attribute a) {
final Function<ByteString, Object, NeverThrowsException> f = byteStringToJson(a.getAttributeDescription());
diff --git a/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties b/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
index 4a6f321..bc38afb 100644
--- a/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
+++ b/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
@@ -107,3 +107,5 @@
ERR_RUNTIME_EXCEPTION_61=A runtime exception occurred wile processing the request '%s': '%s'
ERR_PASSWORD_MODIFY_REQUEST_IS_INVALID_62=The password modify request has been rejected because it is invalid. \
A password modify request may contain two string valued fields 'oldPassword' and 'newPassword'
+ERR_CONFIG_INVALID_TRUST_MANAGER_63=The trust-manager defined in '%s' is invalid: %s
+ERR_CONFIG_INVALID_KEY_MANAGER_64=The key-manager defined in '%s' is invalid: %s
--
Gitblit v1.10.0