JWT / OpenID Connect
OpenID Connect is a protocol built on top of OAuth2 for enabling standardized exchange of identity information – the specification is available here: http://openid.net/connect/
Ceptor can act as both an Identity Provider, and a Relying Party.
Please read the Oauth2 section here: OAuth2, and consider JWT as a specific type of Oauth2 bearer token, and OpenID Connect as a specification of the contents of the token – this is a bit simplified view, since OpenID Connect covers more than that, but it sets the scene.
The Ceptor Authentication plugin; JWTAuthenticationPlugin implements support for both issuing OpenID Connect access and ID tokens, and for consuming tokens, parsing and validating them and making their information available to applications protected by Ceptor.
Usage Scenario
The following describes a typical scenario, where Ceptor is used as an Identity Provider, in this example; https://www.portalprotect.dk
In OpenID Connect, a “Relying Party” is the application or website which wants to access the authenticated user’s information – in this example https://www.example.com
The user browses https://www.example.com, and selects to authenticate – here, we assume that https://www.example.com is already registered as a valid client ID at www.portalprotect.dk
Now, the browser is being redirected to: https://www.portalprotect.dk/oauth2/auth?response_type=id_token&client_id=https%3A%2F%2Fwww.example.com&redirect_uri=https%3A%2F%2Fexample.com%2Foauth2response&scope=profile+email&state=12345 this is the authentication endpoint.
If we look at the parameters, there are:
response_type: id_token – possible types include code, token and id_token.
client_id: https://www.example.com – this is the registered client ID.
redirect_uri: https://example.com/oauth2response - The url to redirect back to
scope: profile+email – The type of information example.com wishes to get.
state: 12345 – sent back to example.com along with the token – allows it to correlate state.
Here, at portalprotect.dk, the application checks if the user is currently authenticated or not – if not, he is asked to login – e.g. using userid/password, nemid or any of the other available authentication methods. After login, the user is shown a page where he can see what information that example.com asks for, and he is given the option to confirm or deny providing this information to example.com.
Once he confirms, a JWT ID Token is created, and sent back to example.com on the url: https://example.com/oauth2response#id_token=xxxxxxx_jwt_token_xxxxxx&state=12345
The actual id_token contents is not sent to the server (notice the # in the url) but is accessible to javascript on the page. If the server wishes to get the token directly, a respone_type of “code” should be used instead – in this case, example.com is issued a short-living access code, and it needs to retrieve that code by making a call to https://www.portalprotect.dk/oauth2/token with its client id, client secret (password), and the code as parameter. Then it will get the id_token and eventually an access token returned.
Architecture / Design
The JWTAuthentication plugin has a list of tokens configured – a token configuration element contains information about token issuer, signing algorithm, certificates/keys needed to issue or verify it etc. You need a token configuration for each type of JWT token you wish to issue or consume.
When issuing a token, you can issue it to various clients – a client can be other parts of your site, or it can be 3rd party applications allowed to ask for user identity.
You can control how information about known clients are store – a class implementing the interface IOAuthDataStore provides the required configuration information needed. By default, PortalProtect has an implementation of it that reads the list of clients from either a property file, or from PortalProtect’s configuration for the session controller.
When a token is issued, it contains claims about the user – a claim can be the user’s id, username, and groups etc. – essentially any variable from the PortalProtect session.
When parsing tokens, you can use custom mapping - if defined, it allows you complete control over which fields are mapped, how they are mapped and allows you to do conversions/rewriting of values within them. This is useful, if e.g. the identity provider returns a firstName and a lastName as separate fields, but you need them combined - or if your userid needs to be based upon the userid from the provider, but you do not want an exact copy.
Configuration
This method of configuration described below still works, but is deprecated - instead, refer to Federations which describes a much easier method of configuration using Ceptor Console.
For configuring Ceptor Gateway with OpenID Connect for Identity Provider, please refer to OpenID Connect Identity Provider for details on how to expose OpenID Connect discovery URL and JWK key lists to relying parties.
There is quite a lot of information that can be tailored to your needs by configuration – here is the relevant configuration located within the session controller for the JWTAuthentication plugin:
General
Property | Value |
oauth2.datastoreclass | <classname – default: dk.itp.security.authentication.oauth.data.properties.Oauth2DataStoreProperties > Contains name of class implementing IOAuthDataStore and providing data about registered client IDs. Use dk.itp.security.authentication.oauth.data.OAuthSQLStore to read data from a database, and These two datastores are the same that API Management uses to store its configured API Partners and Applications within, so you should use the matching implementation here. |
oauth2.accesstoken.datastoreclass | <classname - default: dk.itp.security.authentication.oauth.data.AccessTokenMemoryStore > Contains name of a class implementing IAccessTokenDataStore interface, to provide long-term storage of refresh tokens. You can use the dk.itp.security.authentication.oauth.data.AccessTokenMemoryStore for an in-memory store - note that this is only meant for testing. Use dk.itp.security.authentication.oauth.data.AccessTokenSQLStore to store access tokens in a database - in this case, oauth2.datastorename needs to be configured to point to the correct database datastore to use You can also implement your own store, or contact us to obtain another for your use. |
oauth2.datastorename | <Name of datastore - default is datastore-primary> Name of datastore to use when saving access tokens or refresh tokens in a database. |
oauth2.refreshtoken.datastoreclass | <classname - no default> Contains name of a class implementing IRefreshTokenDataStore interface, to provide long-term storage of refresh tokens. You can use the dk.itp.security.authentication.oauth.data.RefreshTokenMemoryStore for an in-memory store - note that this is only meant for testing. Use dk.itp.security.authentication.oauth.data.RefreshTokenSQLStore to store these in a database. We also provide dk.itp.security.authentication.oauth.data.RefreshTokenIgniteStore which uses Apache Ignite with persistence enabled as a clustered datastore. You can also implement your own store, or contact us to obtain another for your use. |
oauth2.defaulttoken | <Token name – default is the first configured token> Sets the name of the token to be used as default if nothing else is provided. |
oauth2.tokens | <List of token names – separated by comma or semicolon> List of the configured tokens. A token contains information about how to generate or parse it, which certificate/keys to use, crypto algorithm etc. |
oauth2.tokens.jwks | <List of token names - separated by comma or semicolon> List the tokens (must be in oauth2.tokens) that should be included in the generated jwks.json key list. Any tokens not mentioned here are hidden and not included in the published jwks URL. |
openid.scopes | <List of scope names – separated by comma or semicolon > List of the configured scopes A scope is e.g. profile or email – it specifies which information to provide and from which variables in the PP session. Special scopes are openid and offline_access - the first is always required to be supported, and the 2nd enables use of refresh_token in the authorization_code flow. |
openid.fields | <List of field names – separated by comma or semicolon > For complex claim values, such as address, this specifies the sub items contained within. |
openid.identityproviders | <List of identity provider names – separated by comma or semicolon > An identity provider is a foreign identity provider where we can lookup an access token and/or id_token using an authorization code. |
Properties for OAuth2 Client Datastore
For the OAuth2DataStoreProperties implementation of the IOAuthDataStore, you can specify the following configuration:
Property | Value |
oauth2.clientpropertiesfile | <File to load remaining properties from> If specified, the remaining properties are loaded from this file – if not, they are instead loaded from the configuration within portalprotect-configuration.xml. |
oauth2.clients | <List of client names – separated by comma or semicolon > List the clients to load. |
The remaining properties start with: oauth2.client.xxxx | Where xxxx is the client name |
.clientid | <Client ID> Contains the client ID – in OpenID connect, the client ID must start with https:// |
.secret | <password – optionally obfuscated/encrypted> Client Secret – used when client gets issued an authorization code, and uses that code plus the client ID and client secret to access an access token and id token. |
.confidential | <boolean> If true, client_secret must be provided on all calls to token URL |
.allowedscopes | <scope name list> Lists the scopes this client is allowed to request – can be used to restrict certain clients so they can only access limited information about the user. |
.allowedredirecturis | <redirect URI list> Lists of valid redirect URIs for this client. |
.allowedlogouturis | <Logout URI list> List of valid logout URIs for this client. |
.validgranttypes | <Grant types> List of valid grant types for this client, can contain; authorization_code,implicit,hybrid,refresh_token See OpenID Connect specification for details. In short; authorization_code requires a server to use the client secret to gain an access token, impliciet allows javascript in the browser to access it, and hybrid is a combination of the two. refresh_token is required to support the offline_access scope. |
.accesstokenvalidityseconds | <Seconds – default: 60> Number of seconds that an issued access token is valid for. |
.maximumexpirationminutes | <Minutes – default: 60> Maximum expiration time of a generated ID token in minutes. |
.refreshtokenvalidityseconds | <Seconds – default: 60> Maximum refreshtoken validity seconds. |
.tokenname | <Token name> Name of token to use for this client ID – specifies the token (algorithm, keys etc.) |
.accesstokentype | <JWT or UUID> Specifies which type of access token to issue – a JWT access token contains information that can be read, where a UUID-type of access tokens is a unique ID key to the access token. The UUID is significantly smaller and does not contain any information in itself. |
Custom Mapping
You can configure a number of custom mappers which you can use from tokens or identity providers.
Property | Value |
oauth2.mappers | <comma or semicolon separated list of mappers> List of mappers to load - e.g. mapper1,mapper2 |
Property Name starts with oauth2.mapper.xxxx where xxxx is the mapper name. | Value |
userid | Mapped value - result is placed into session as user ID |
username | Mapped value - result is placed into session as username |
groups | Mapped value - result is placed into session as list of groups |
customerid | Mapped value - result is placed into session customer ID |
isinternal | Mapped value - result is placed into session as boolean value of isInternal (must be true or false) |
agreementid | Mapped value - result is placed into session as agreement ID |
authlvl | Mapped value - result is placed into session as authentication level - must be a valid integer value |
_state_XXXX where XXXX is name of statevariable | Mapped value - result is placed into session as a state variable - use e.g. _state_username to set the state variable username instead of the username field in the session. |
XXXX where XXXX is name of state variable | Mapped value - result is placed into session as state variable with the given key. |
Mapped Value
The mapped value is a string, where macros are replaced with the appropriate contents - a macro can be%{claim:XXXX} where XXXX is the name of a claim key in the JSON JWT token, or token returned from an identity provider such as facebook.
macros are in the form %{type:name} where type can be claim, base64, urlencode, htmlencode, base64decode, urldecode - the default is claim if not specified. If set to e.g. base64, the value will be treated as base64 and decoded.
Like with Ceptor Gateway - Scripts and Macros you can use scripts (javascript, python or groovy) and %{rewrite} macros to do more advanced transformations of values. When scripts are used, the variable context will point to an object which contains two variables; session pointing to the users Ceptor Session, and jo pointing to a JSONObject containing the claims to map.
Examples:
oauth2.mapper.xxxx.username=%{claim:firstName} %{claim.lastName}
oauth2.mapper.xxxx.salary=%{script}salary(); function salary() {var json = JSON.from(context.jo.toJSONString()); return (json.monthlySalary * 12) + json.yearlyBonus;}
oauth2.mapper.xxxx.valuesAreFrom=Some Custom Provider
Token
For each token contained in oauth2.token, the following is configured:
Property Name starts with oauth2.token.xxxx | Value |
.issuer | <Issuer name> Name of issuer – e.g. https://www.portalprotect.dk |
.validaudiences | <List of names, separated by comma or semicolon> List of audience names that are valid for this token |
.algorithm | <JWT signing algorithm name – default is RS256> Must be a valid JWT signing algorithm name, supported algorithms are; HS256, HS384, HS512, RS256, Rs384, RS512, ES256, ES384, ES512, PS256, PS384, PS512 |
.keyid | <Key identifier> Key ID – for the JWT header “kid” field. |
.claims | <List of claims – default: “sub=userid;groups=groups;name=username” > See the section about claim name/value paris below |
.usernameAttributeName | <Claim name – default: “name”> When parsing a claim issued by someone else – which attribute to look for the users name within. |
.useridAttributeName | <Claim name – default: “sub”> When parsing a claim issued by someone else – which attribute to look for the users id witin. |
.roleAttributeName | <Claim name – default: “groups”> When parsing a claim issued by someone else – which attribute to look for the list of user groups within. |
.rolePattern | <Stringmatcher pattern – default: “*”> Which roles/groups to include in the token when creating claims. Example: “^admin*” to add all group names except those starting with admin. |
.attributesToStoreInSession | <Stringmatcher pattern – default: “*”> Which attributes to store within PPs session when parsing a token issued by others. |
.relaxKeyChecks | <true | false – default: “false”> Set to true to relax key checking, meaning to allow weak keys to be used for signing. |
.openidconnect | <true | false – default: “false”> Specifies if the token should be openid connect compliant or just a regular JWT token. |
.expiresAtExactTime | <true | false – default: “false”> If true, when parsing a JWT token, with an expiration time within it, the token will expire at that exact time/date and will no longer be valid. If false, the expiration time will only be used at authentication time, and the resulting session will expire using normal idle timeout settings. |
.requireSubject | <true | false - default: "true"> When parsing tokens, normally they require a subject (the "sub" claim) - if you set this to false, tokens are accepted without a subject. (Requires Ceptor 6.2.7 or later) |
.signerCertificates | <List of certificate filenames> List of files containing valid certificates that can be used to verify the signature of this token. |
.signerCertificatesURL | <URL> Place to load additional signer certificates from – e.g. https://www.googleapis.com/oauth2/v1/certs The certificates must be in a JSON object, with key/value pairs where the value is the certificate. |
.signerCertificatesRefreshIntervalMinutes .signerCertificatesRefreshIntervalHours | <Number of minutes / hours> - default: 60 minutes. If number of minutes is not specified, number of hours is read - otherwise, only number of minutes is used. Specifies the number of minutes to cache the certificates read from the authentication provider - set to 0 to re-read it every time. |
.acceptedServerCertificates | <List of certificate files, separated by semicolon or comma> If the default cacerts trusted CA/root certificates is not enough, you can add additional certificates here. This applies to the signerCertificatesURL |
.verifyServerCert | <true or false, default: true> Set to false to disable validation of the SSL server certificate for the signerCertificatesURL |
.verifySSLHostname | <true or false, default: true> Set to false to dsable hostname validation – when true, the hostname in the URL must match the hostname in the SSL server certificate. |
.keystore.provider | <JCE provider name> Name of the JCE provider to use when loading the keystore |
.keystore.type | <JCE keystore type – default: “PKCS12”> Specifies the format of the keystore, e.g. PKCS12 or JKS. |
.keystore.file | <filename> Specifies the filename of the keystore to load the keys from. |
.keystore.password | <password> Password for the keystore – can optionally be encrypted/obfuscated |
.keystore.privalias | <alias name> Alias of the private key within the keystore to use – if no alias, the first key found will be used. |
.keystore.certalias | <alias name> Alias of the certificate within the keystore to use – if no alias, uses the first found certificate. |
.jceprovider | <JCE provider name> Name of the JCE provider to use when signing or validating signature of the JWT token. |
.secretkey | <Secret key> For algorithms starting with HS, a secret shared key is used – this should be avoided in production environments, since anyone in possession of the shared key used to validate JWT tokens can also is the same key to issue new tokens. |
.clockSkewSeconds | <Time difference in seconds> - Default: 0, Requires v5.61 Set the clock skew allowed when validating expiration / not-before timestamps in the token - allows adjusting for time difference between machines. |
.expirationminutes | <Minutes> - Default: 10 When creating a token, this is the expiration time as minutes in the future. |
.notBeforeMinutesInPast | <Minutes> - Default: 2 When creating a token, this is the number of minutes in the past to set nbf (not before) attribute to. |
.customfieldmapper | <Name of custom mapper> If specified, when tokens are parse, they are not just copied - instead, the fields in the custom mapper are constructed based upon the claims provided as input. Note that attributesToStoreInSession has no effect if custom field mapping is enabled. |
.script.accesstoken | <Script or Mapper> Script/Mapper that has a chance to modify the access token before it is signed and issued - it must return the new JSON value. |
.script.idtoken | <Script or Mapper> Script/Mapper that has a chance to modify the ID token before it is signed and issued - it must return the new JSON value. |
.script.userinfo | <Script or Mapper> Script/Mapper that has a chance to modify the userinfo before it is sent - it must return the new JSON value. |
Scopes
OpenID Scope names are mapped to claim names and attributes by configuration.
Property Name starts with openid.scope.xxxx | Value |
.description | <String> Description of the scope |
.idtoken | <List of claim name/value pairs> List of claim name/value pairs to include in the ID token for this scope. |
.accesstoken | <List of claim name/value pairs> List of claim name/value pairs to include in the access token – (if the access token is a JWT token and not an UUID) for this scope. |
.userinfo | <List of claim name/value pairs> List of claim name/value pairs to include in the userinfo for this scope. The userinfo is a JSON object that can be requested from https://xxxx/oauth2/userinfo by providing a valid access token as input. |
Claim name/value pairs
A list of claim names is specified as e.g. sub=userid;groups=groups;name=username – it is a semicolon separated list of key/values.
The key is the name of the claim in the JWT token, and the value has special meaning; it refers to attributes within PortalProtect’s session;
Value | Meaning |
null | Leave the claim out – has the same meaning as if the claim was not present at all, but can be used as a placeholder in the configuration where it can be later changed to another attribute. |
userid | Users ID |
username | Users name |
sessionid | PP Session ID |
customerid | Customer ID |
isinternal | True if user is internal, false if not |
agreementid | Agreement ID |
authmethod | Authentication method |