OpenID Connect Support
The Payara API provides a @OpenIdAuthenticationDefinition
annotation that creates an authorization mechanism for OpenID Connect support.
This works in the same way as other authorization mechanisms in the Java EE Security API.
Usage
The OpenID Connect authentication mechanism is defined through the @OpenIdAuthenticationDefinition
annotation.
Specifying this in a valid place as defined by the Java EE Security API will create the mechanism.
Often this may mean that any class is a valid position.
Example
Here’s an example that configures a OpenID Connect client:
@OpenIdAuthenticationDefinition(
providerURI = "https://sample-openid-server.com",
clientId = "87068hgfg5675htfv6mrucov57bknst.apps.sample.com",
clientSecret = "{my-secret}",
redirectURI = "${baseURL}/callback",
extraParameters = {
"testKey=testValue",
"testKey2=testValue2"
}
)
public class SecurityBean {
}
See this sample project for a more detailed example.
When defining a OpenID Connect flow within an application deployed on Payara Server, it is possible to retrieve the access token, identity token, user claims and the other authentication information within any bean in the scope of the callback/redirectURI resource used to configure the authentication:
@WebServlet("/callback")
public class CallbackServlet extends HttpServlet {
@Inject
OpenIdContext context;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
//Here's the caller name
out.println(context.getCallerName());
//Here's the caller groups
out.println(context.getCallerGroups());
//Here's the unique subject identifier within the issuer
out.println(context.getSubject());
//Here's the access token
out.println(context.getAccessToken());
//Here's the identity token
out.println(context.getIdentityToken());
//Here's the user claims
out.println(context.getClaimsJson());
}
}
The original protected URL called is stored so that a redirect can be issued to this page after the callback from the OpenIdConnect provider is handled.
Only some basic redirect is supported namely a GET. Only the URL and query parameters are available, not the headers and the request body. |
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// handling the OpenIdContext information. See previous example
// Now return to original requested page
response.sendRedirect(request.getSession().getAttribute(OpenIdConstant.ORIGINAL_REQUEST).toString());
}
Configuration
OpenID Client can be configured with both @OpenIdAuthenticationDefinition
annotation attributes and Microprofile Config properties.
The annotation and MicroProfile properties has several configuration options.
They are detailed below.
Option | MP Config property | Required | Description | Default value | Requirements |
---|---|---|---|---|---|
|
|
true |
The provider uri to discover the metadata of the OpenID Connect provider. |
The endpoint must be HTTPS. |
|
|
|
true |
The client identifier issued when the application was registered. |
N/A. |
|
|
|
true |
The client secret for the registered application. |
N/A. |
|
|
|
true |
The URL to redirect the user to upon successful authentication. |
${baseURL}/Callback |
Must be equal to one set in the OpenID Connect provider. |
|
|
false |
The scopes requested from the OpenID Connect provider. |
{openid, email, profile} |
N/A. |
|
|
false |
Response Type value defines the processing flow to be used. |
code |
N/A. |
|
|
false |
Informs the Authorization Server of the mechanism to be used for returning parameters from the Authorization Endpoint. |
N/A. |
|
|
|
false |
The prompt value specifies whether the authorization server prompts the user for re-authentication and consent. |
N/A. |
|
|
|
false |
The display value specifying how the authorization server displays the authentication and consent user interface pages. |
page |
N/A. |
|
|
false |
Enables string value used to mitigate replay attacks. |
true |
N/A. |
|
|
false |
If enabled state & nonce value stored in session otherwise in cookies. |
true |
N/A. |
|
|
false |
Sets the connect timeout(in milliseconds) for Remote JWKS retrieval. |
500 |
Value must not be negative and if value is zero then infinite timeout. |
|
|
false |
Sets the read timeout(in milliseconds) for Remote JWKS retrieval. |
500 |
Value must not be negative and if value is zero then infinite timeout. |
|
|
false |
Enables or disables the automatically performed refresh of Access and Refresh Token. |
false |
N/A. |
|
|
false |
Sets the minimum validity time(in milliseconds) the Access Token must be valid before it is considered expired. |
10000 |
Value must not be negative. |
|
|
false |
Defines the name of callerName claim and maps the claim’s value to caller name value in IdentityStore#validate. |
preferred_username |
N/A. |
|
|
false |
Defines the name of callerGroups claim and maps the claim’s value to caller groups value in IdentityStore#validate. |
groups |
N/A. |
|
false |
An array of extra options to be sent to the OpenID Connect provider. |
Must be in the form |
||
|
@LogoutDefinition |
Defines the functionality that is performed when the user logs out and defines the RP Session Management configuration. |
If both an annotation attribute and a MicroProfile Config property are defined for the same option
then the MicroProfile Config property value always takes precedence over the @OpenIdAuthenticationDefinition annotation value.
|
If you are using MicroProfile Config properties and the value contains a placeholder that should not be resolved by MicroProfile Config
itself but by the Server (like payara.security.openid.redirectURI=${baseURL}/Callback ) make sure the value is properly escaped. It will otherwise result in NoSuchElementException . Here’s an example how such a value should be escaped: payara.security.openid.redirectURI=\\${baseURL}/Callback
|
Expression Language Support
Additionally, the @OpenIdAuthenticationDefinition
supports the use of expression language (EL) notation for dynamic configuration scenarios.
This means that you can use any CDI bean properties to set the OpenID Connect configuration like this:
@OpenIdAuthenticationDefinition(
providerURI="#{openidConfigBean.tokenEndpointURL}",
clientId="#{openidConfigBean.clientId}",
clientSecret="#{openidConfigBean.clientSecret}",
redirectURI="#{openidConfigBean.redirectURI}"
)
public class SecurityBean {
}
By default, the EL expressions are evaluated only once after the application is loaded and the evaluated values are then remembered until the application is reloaded, for performance reasons. This means that although the configuration can be evaluated dynamically the first time it’s needed, it’s not possible to change the configuration later on. If you need to dynamically modify the configuration during the lifetime of the application, follow the next section about multitenancy support. |
Multitenancy Support (Session-scoped Configuration)
By default, the same configuration of the OpenID connector is applied for the whole application, for all authentication attempts. This is for performance reasons. The OpenID connector also supports re-evaluating the configuration for each user session, before each authentication attempt. This is useful in a multitenant scenario to define a different configuration for each tenant. It’s also useful if the user should be able to select which provider they want to use to authenticate.
To enable re-evaluation of the configuration for each user session, set the MicroProfile Configuration property payara.security.openid.sessionScopedConfiguration
to true
. To specify it directly in the application, you can place it in the microprofile-config.properties file in the META-INF
directory on the classpath (in a WAR application it could be in WEB-INF/classes/META-INF
).
With this enabled, it’s possible to use EL expressions to dynamically adjust the configuration before each authantication attempt, e.g. based on any information in the incoming HTTP request. The information about the HTTP request can be retrieved from a HttpServletRequest
object injected using @Inject
.
It’s not possible to use a different configuration for just a subset of secured resources. Once a user is authenticated, the authentication information is saved in the HTTP session. All secured resources will be accessed using the same user, having the same roles, until the user logs out. |
Example multitenant authentication
In this example, we’ll:
-
Enable session-scoped OpenID Connect configuration
-
Resolve the tenant name from an HTTP request query parameter
-
Use the tenant name to read the configuration value from the respective MicroProfile Config property
-
Retrieve the value from an EL expression defined in the
@OpenIdAuthenticationDefinition
annotation
For example, the tenant can also be resolved from a cookie, which is set the first time a user loads the application; from the domain name in the URL (if different tenants use a different domain name to access the same application); from a path prefix that follows the context root and prepends all application URLs (e.g. contextroot/tenant1/index.xhtml, contextroot/tenant2/index.xhtml). |
Create a file microprofile-config.properties
in your application (for a WAR application it would be in the WEB-INF/classes/META-INF
directory), with the following contents:
payara.security.openid.tenant1.providerURI=<TENANT1_OPENID_PROVIDER_URI>
payara.security.openid.tenant2.providerURI=<TENANT2_OPENID_PROVIDER_URI>
payara.security.openid.sessionScopedConfiguration=true
This will provide configuration for tenant1
and tenant2
tenants. For each additional tenant, add a new line for its providerURI
.
Create an OpenidConfigBean
class with the tokenEndpointURL
method. This class will be a CDI bean that injects HttpServletRequest
to get information about which tenant to use. It will also inject Config
to retrieve the configuration about each tenant from the microprofile-config.properties
file:
@Named
public class OpenidConfigBeanEL {
@Inject
HttpServletRequest request;
@Inject
Config config;
private static final String BASE_OPENID_KEY = "payara.security.openid";
public String getTokenEndpointURL() {
String tenant = getTenant(request); // a custom method to decide which tenant to use
return config
.getOptionalValue(BASE_OPENID_KEY + "." + tenant + ".providerURI", String.class)
// e.g. payara.security.openid.tenant1.providerURI for "tenant1" tenant
.orElseGet(() -> {
// read config for the "tenant1" tenant by default
return config.getValue(BASE_OPENID_KEY + ".tenant1.providerURI", String.class);
});
}
private String getTenant(HttpServletRequest request) {
return request.getParameter("tenant"); // resolves the tenant name from a query parameter
}
}
Finally, configure the OpenID Connector using the OpenIdAuthenticationDefinition
annotation that references the getTokenEndpointURL()
in an EL expression:
@OpenIdAuthenticationDefinition(
providerURI = "#{openidConfigBean.tokenEndpointURL}",
clientId = CLIENT_ID_VALUE,
clientSecret = CLIENT_SECRET_VALUE,
redirectURI = "${baseURL}/Callback"
)
public class SecurityBean {
}
Logout Functionality
With the logout
parameter of the OpenIdAuthenticationDefinition
you can define the behavior when the user logs out of the application and defines how the RP session is managed.
Option | MP Config property | Required | Description | Default value |
---|---|---|---|---|
|
payara.security.openid.provider.notify.logout |
false |
Notify the OIDC provider (OP) that the user has logged out of
the application and might want to log out of the OP as well. If true then
after having logged out the user from RP, redirects the End-User’s User
Agent to the OP’s logout endpoint URL. This URL is normally obtained via
the |
false |
|
payara.security.openid.logout.redirectURI |
false |
The post logout redirect URI to which the RP is requesting that the End-User’s User Agent be redirected after a logout has been performed. If redirect URI is empty then redirect to OpenID connect provider authorization_endpoint for re-authentication. |
|
|
payara.security.openid.logout.access.token.expiry |
false |
Whether the application session times out when the Access Token expires. |
false |
|
payara.security.openid.logout.identity.token.expiry |
false |
Whether the application session times out when the Identity Token expires. |
false |
A programmatic logout is performed by calling OpenIdContext#logout()
which invalidates the RP’s active OpenId Connect session. If fish.payara.security.annotations.LogoutDefinition#notifyProvider
is set to true then it redirects the End-User’s User Agent to the end_session_endpoint
to notify the OP that the user has logged out of the RP’s application. It will also ask the user whether they want to logout from the OP as well. After successful logout, the End-User’s User Agent redirects back to the RP’s post_redirect_uri
configured via fish.payara.security.annotations.LogoutDefinition#redirectURI
.
Client Secret Aliasing
The client secret can be input directly, or for added security it can be aliased using any of the following features:
Fetch Caller Data
As OpenId Connect Client is built on top of Jakarta EE Security API,
therefore javax.security.enterprise.SecurityContext
interface can provide
caller info which is available as a CDI bean and can be injected into any context-aware instance.
The Payara API also provides a fish.payara.security.openid.api.OpenIdContext
interface which is also available as a CDI bean and consist of the following methods:
-
The
getCallerName()
method - Gets the caller name of the currently authenticated user. -
The
getCallerGroups()
method - Gets the groups associated with the caller. -
The
getSubject()
method - Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client. -
The
getTokenType()
method - Gets the token type value. The value MUST be Bearer or another token_type value that the Client has negotiated with the Authorization Server. -
The
getAccessToken()
method - Gets the authorization token that was received from the OpenId Connect provider. -
The
getIdentityToken()
method - Gets the identity token that was received from the OpenId Connect provider. -
The
getRefreshToken()
method - Returns the refresh token that is used by OIDC client to get a new access token. -
The
getExpiresIn()
method - Return the time that the access token is granted for, if it is set to expire. -
The
getClaimsJson()
method - Gets the User Claims JSON that was received from the userinfo endpoint. -
The
getClaims()
method - Gets the User Claims that were received from the userinfo endpoint. -
The
getProviderMetadata()
method - The OpenId Connect Provider’s metadata document fetched via provider URI.
Bearer Authentication and Authorization
In order to authenticate and authorize calls between services using the OpenID mechanism, it is possible to use authorization compatible with RFC 6750. In this case, the access token presented to the resource service is an JWT token that is used to verify that the caller has access to OAuth2 protected resources.
Obtaining JWT Token
Obtaining the token is specific to the OAuth provider and the application. The usual approach is using Client Credentials Grant, where an application posts its clientId and secret to identity provider and receives access and refresh tokens in return.
Passing Token To The Resource Service
The obtained access token is passed with every request to the resource service by adding it into the Authorization
HTTP header:
Authorization: Bearer access__token
Processing Bearer Authorization
When Bearer authorization header is present in the request, the provided token is verified. It’s validated that it comes from the expected issuer and hasn’t expired.
Compared to the normal browser flow, no groups are automatically assigned to the identity. The reason for this is that machine-to-machine communication tends to be much more fine-grained and services might want to check more claims, such as audience .
|
The resource service is required to map the information in the JWT token to groups utilizing the IdentityStore
interface.
OpenID connector provides the following classes to make this process possible:
-
AccessTokenCallerPrincipal
is a specific caller principal subclass that contains access to all claims of passed JWT token -
BearerGroupIdentityStore
is convenience base implementation ofIdentityStore
that handles the cast.
@ApplicationScoped
@DeclareRoles({"user", "calendar-reader"})
public class Auth0BearerIdentityStore extends BearerGroupsIdentityStore {
@Override
protected Set<String> getCallerGroups(AccessTokenCallerPrincipal callerPrincipal) {
if (callerPrincipal.hasAudience("https://example.org/api/user")) {
// if the token is for USER api, set this group
return Set.of("user");
}
if (callerPrincipal.hasAudience("https://example.org/api/delegate")
// delegate API is further constrained by scope
&& callerPrincipal.getAccessToken().getScope().contains("read:calendar")) {
return Set.of("calendar-reader");
}
return Set.of();
}
}
Payara Platform also provides similar functionality by MicroProfile JWT Authentication, which is however limited only to securing JAX-RS resources. On the other hand, the OpenID Connect Bearer Authentication and Authorization feature is better aligned with the OpenID Connect support in Payara Platform and can also be used to secure other resources like servlets. |
Integration with specific providers
Google integration
The Payara API provides the in-built support for Google OpenID Provider via the @GoogleAuthenticationDefinition
annotation.
Request Refresh Token
To enable the refresh token feature, set the tokenAutoRefresh
to true
and add the access_type
parameter value to offline
so that application can refresh access tokens when the user is not present at the browser.
If application requests offline
access then the application can receive access and refresh token.
Once the application has a refresh token, it can obtain a new access token at any time or as older ones expire.
Otherwise, If application requests online
access, your application will only receive an access token
@GoogleAuthenticationDefinition(
providerURI="#{openidConfigBean.tokenEndpointURL}",
clientId="#{openidConfigBean.clientId}",
clientSecret="#{openidConfigBean.clientSecret}",
...
tokenAutoRefresh = true,
extraParameters = {"access_type=offline", "approval_prompt=force"}
)
public class SecurityBean {
}
Azure AD integration
The Payara API also provides the in-built support for Azure AD OpenID Provider via the @AzureAuthenticationDefinition
annotation.
Request Refresh Token
To receive the refresh token, set the tokenAutoRefresh
to true and explicitly add the offline_access
scope to the definition.
@AzureAuthenticationDefinition(
providerURI="#{openidConfigBean.tokenEndpointURL}",
clientId="#{openidConfigBean.clientId}",
clientSecret="#{openidConfigBean.clientSecret}",
...
tokenAutoRefresh = true,
scope = {OPENID_SCOPE, EMAIL_SCOPE, PROFILE_SCOPE, OFFLINE_ACCESS_SCOPE}
)
public class SecurityBean {
}
Groups mapping
-
To add the groups to the registered application, Sign in to the Azure portal > Azure Active Directory > Manage > App registrations > select your application:
-
You may also add the custom roles via Roles and administrators under the Manage section:
-
Now to map group claims, select Token configuration under the Manage section:
-
Press Add groups claim button to select group types and customize Id and/or Access token properties:
-
Groups claim can also be defined via Azure Manifest under the Manage section which is a JSON configuration file.
-
To retrieve and map the caller name & groups from token claims, set the caller name & group claim definition to
preferred_username
&groups
.
@AzureAuthenticationDefinition( providerURI="#{openidConfigBean.tokenEndpointURL}", clientId="#{openidConfigBean.clientId}", clientSecret="#{openidConfigBean.clientSecret}", ... claimsDefinition = @ClaimsDefinition( callerGroupsClaim = "groups", callerNameClaim = "preferred_username" ) ) public class SecurityBean { }
Keycloak integration
Keycloak is Open Source Identity and Access Management Server, which is a OAuth2 and OpenID Connect(OIDC) protocol complaint. In this section, the basic steps are described to setup Keycloak OpenId provider.For more details about Keycloak configuration options, please visit to the official documentation: https://www.keycloak.org/documentation.html
-
Refer Keycloak getting started documentation to run and setup keycloak.
-
After Keycloak setup done, login to Keycloak admin console and add the new realm by pressing the Add Realm button:
-
Copy the OpenId endpoint configuration URL from endpoint section:
-
Now add the Role that will be used by the application to define which users will be authorized to access the application.
-
Create the Groups:
-
Add the User:
-
After the user is created, set a new password for the user:
-
Now map the user to roles. Click on Role Mappings tab and assign the roles to the user from the available roles:
-
Assign the user to the groups. Click on Groups tab and join the groups from the available groups:
-
Create the OpenId Client by clicking the Client option from sidebar and press the create button: Enter the Client ID and select the Client Protocol openid-connect and press Save.
-
After the openid client is created change its Access Type to confidential and enter the valid Redirect URIs:
-
Next copy the client secret from Credentials tab.
Here’s an example that configures a OpenID Connect client for Keycloak provider. To test the KeyCloak OpenId provider, enter the copied Client Secret, Client ID (client name) and the endpoint configuration URL:
@OpenIdAuthenticationDefinition(
providerURI = "http://${keycloak-host}:${keycloak-port}/auth/realms/test-realm",
clientId = "test-client",
clientSecret = "1f6744ae-d7e7-4876-bc44-78fb691316a1"
...
)
public class SecurityBean {
}
Groups mapping
-
To get the groups details in token claims, navigate to Keycloak admin console > OpenId Client > Mappers tab > press create button > Select Group Membership mapper type > enter the Name and Token Claim Name > press Save.
-
To retrieve and map the caller name & groups from token claims, set the caller name & group claim definition to
preferred_username
&groups
.
@OpenIdAuthenticationDefinition( providerURI = "http://${keycloak-host}:${keycloak-port}/auth/realms/test-realm", clientId = "test-client", clientSecret = "1f6744ae-d7e7-4876-bc44-78fb691316a1" ... claimsDefinition = @ClaimsDefinition( callerGroupsClaim = "groups", callerNameClaim = "preferred_username" ) ) public class SecurityBean { }