All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.wildfly.security.soteria.original.OpenIdAuthenticationMechanism Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2021, 2022 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * Contributors:
 *   2021 : Payara Foundation and/or its affiliates
 *      Initially authored in Security Connectors
 */
package org.wildfly.security.soteria.original;


import static jakarta.security.enterprise.AuthenticationStatus.SEND_FAILURE;
import static jakarta.security.enterprise.AuthenticationStatus.SUCCESS;
import static jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant.ERROR_DESCRIPTION_PARAM;
import static jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant.ERROR_PARAM;
import static jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant.EXPIRES_IN;
import static jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant.ID_TOKEN_HINT;
import static jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant.ORIGINAL_REQUEST;
import static jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant.POST_LOGOUT_REDIRECT_URI;
import static jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant.REFRESH_TOKEN;
import static jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant.STATE;
import static jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant.TOKEN_TYPE;
import static jakarta.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;
import static jakarta.security.enterprise.identitystore.CredentialValidationResult.NOT_VALIDATED_RESULT;
import static jakarta.ws.rs.core.Response.Status.OK;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.glassfish.soteria.Utils.isEmpty;
import static org.glassfish.soteria.Utils.isOneOf;

import java.io.IOException;
import java.io.Serializable;
import java.io.StringReader;
import java.util.Optional;
import java.util.logging.Logger;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.glassfish.soteria.mechanisms.openid.OpenIdState;
import org.glassfish.soteria.mechanisms.openid.domain.LogoutConfiguration;
import org.glassfish.soteria.mechanisms.openid.domain.OpenIdConfiguration;
import org.glassfish.soteria.mechanisms.openid.domain.RefreshTokenImpl;
import org.glassfish.soteria.servlet.HttpServletRequestDelegator;
import org.glassfish.soteria.servlet.HttpStorageController;
import org.glassfish.soteria.servlet.RequestData;

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.Typed;
import jakarta.inject.Inject;
import jakarta.json.Json;
import jakarta.json.JsonNumber;
import jakarta.json.JsonObject;
import jakarta.json.JsonReader;
import jakarta.security.auth.message.callback.CallerPrincipalCallback;
import jakarta.security.enterprise.AuthenticationException;
import jakarta.security.enterprise.AuthenticationStatus;
import jakarta.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism;
import jakarta.security.enterprise.authentication.mechanism.http.HttpMessageContext;
import jakarta.security.enterprise.identitystore.CredentialValidationResult;
import jakarta.security.enterprise.identitystore.IdentityStoreHandler;
import jakarta.security.enterprise.identitystore.openid.RefreshToken;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;

/**
 * The AuthenticationMechanism used to authenticate users using the OpenId
 * Connect protocol
 * 
* Specification Implemented : * http://openid.net/specs/openid-connect-core-1_0.html * * @author Gaurav Gupta * @author Rudy De Busscher * @author Arjan Tijms */ // +--------+ +--------+ // | | | | // | |---------------(1) Authentication Request------------->| | // | | | | // | | +--------+ | | // | | | End- |<--(2) Authenticates the End-User---->| | // | RP | | User | | OP | // | | +--------+ | | // | | | | // | |<---------(3) returns Authorization code---------------| | // | | | | // | | | | // | |------------------------------------------------------>| | // | | (4) Request to TokenEndpoint for Access / Id Token | | // | OpenId |<------------------------------------------------------| OpenId | // | Connect| | Connect| // | Client | ----------------------------------------------------->|Provider| // | | (5) Fetch JWKS to validate ID Token | | // | |<------------------------------------------------------| | // | | | | // | |------------------------------------------------------>| | // | | (6) Request to UserInfoEndpoint for End-User Claims | | // | |<------------------------------------------------------| | // | | | | // +--------+ +--------+ @ApplicationScoped @Typed(OpenIdAuthenticationMechanism.class) public class OpenIdAuthenticationMechanism implements HttpAuthenticationMechanism { private static final Logger LOGGER = Logger.getLogger(OpenIdAuthenticationMechanism.class.getName()); public static final String ORIGINAL_REQUEST_DATA_JSON = "org.glassfish.soteria.original.request.json"; private static final String SESSION_LOCK_NAME = OpenIdAuthenticationMechanism.class.getName(); @Inject private OpenIdConfiguration configuration; @Inject private OpenIdContextImpl context; private IdentityStoreHandler identityStoreHandler; @Inject private AuthenticationController authenticationController; @Inject private TokenController tokenController; @Inject private StateController stateController; @Inject Instance storeHandlerInstance; private static class Lock implements Serializable { private static final long serialVersionUID = 1L; } @PostConstruct void init() { if (storeHandlerInstance.isResolvable()) { identityStoreHandler = storeHandlerInstance.get(); return; } throw new IllegalStateException("Cannot get instance of IdentityStoreHandler\n" + "@Inject IdentityStoreHandler is unsatisfied."); } @Override public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpContext) throws AuthenticationException { if (isNull(request.getUserPrincipal())) { LOGGER.fine("UserPrincipal is not set, authenticate user using OpenId Connect protocol."); // User is not authenticated // Perform steps (1) to (6) return authenticate(request, response, httpContext); } // User has been authenticated in request before // Try-catch-block taken from AutoApplySessionInterceptor // We cannot use @AutoApplySession, because validateRequest(...) must be called on every request // to handle re-authentication (refreshing tokens) // https://stackoverflow.com/questions/51678821/soteria-httpmessagecontext-setregistersession-not-working-as-expected/51819055 // https://github.com/javaee/security-soteria/blob/master/impl/src/main/java/org/glassfish/soteria/cdi/AutoApplySessionInterceptor.java try { httpContext.getHandler().handle(new Callback[]{ new CallerPrincipalCallback(httpContext.getClientSubject(), request.getUserPrincipal())} ); } catch (IOException | UnsupportedCallbackException ex) { throw new AuthenticationException("Failed to register CallerPrincipalCallback.", ex); } // 1. Check for refreshing token boolean accessTokenExpired = context.getAccessToken().isExpired(); boolean identityTokenExpired = context.getIdentityToken().isExpired(); if ((accessTokenExpired || identityTokenExpired) && configuration.isTokenAutoRefresh()) { if (accessTokenExpired) { LOGGER.fine("Access Token is expired. Request new Access Token with Refresh Token."); } if (identityTokenExpired) { LOGGER.fine("Identity Token is expired. Request new Identity Token with Refresh Token."); } return this.reAuthenticate(httpContext); } // 2. Check for logging out LogoutConfiguration logout = configuration.getLogoutConfiguration(); if (logout.isIdentityTokenExpiry()) { LOGGER.log(FINE, "UserPrincipal is set, check if Identity Token is valid."); } if (logout.isAccessTokenExpiry()) { LOGGER.log(FINE, "UserPrincipal is set, check if Access Token is valid."); } if ((logout.isAccessTokenExpiry() && accessTokenExpired) || (logout.isIdentityTokenExpiry() && identityTokenExpired)) { logout(request, response); return SEND_FAILURE; } // 3. Ignore token expired, or token is not expired return SUCCESS; } @Override public void cleanSubject(HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpMessageContext) { logout(request, response); } private AuthenticationStatus authenticate(HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpContext) { Optional receivedState = OpenIdState.from(request.getParameter(STATE)); if (receivedState.isEmpty() && httpContext.isProtected() && isNull(request.getUserPrincipal())) { // (1) The End-User is not authenticated. return authenticationController.authenticateUser(request, response); } if (receivedState.isPresent()) { String callbackUrl = configuration.buildRedirectURI(request); String orginalUrl = getOriginalUrl(request, response); String requestUrl = request.getRequestURL().toString(); if (configuration.isRedirectToOriginalResource()) { if (!isOneOf(requestUrl, orginalUrl, callbackUrl)) { LOGGER.log(INFO, "OpenID request URL {0} not matched with either callback {1} or original URL {2}", new Object[]{requestUrl, callbackUrl, orginalUrl}); return httpContext.notifyContainerAboutLogin(NOT_VALIDATED_RESULT); } } else { if (!isOneOf(requestUrl, callbackUrl)) { LOGGER.log(INFO, "OpenID request URL {0} not matched with callback URL {1}", new Object[]{requestUrl, callbackUrl, orginalUrl}); return httpContext.notifyContainerAboutLogin(NOT_VALIDATED_RESULT); } } Optional expectedState = stateController.get(request, response); if (!expectedState.isPresent()) { LOGGER.fine("Expected state not found"); return httpContext.notifyContainerAboutLogin(NOT_VALIDATED_RESULT); } if (!expectedState.equals(receivedState)) { LOGGER.fine("Inconsistent received state, value not matched"); return httpContext.notifyContainerAboutLogin(INVALID_RESULT); } // (3) Successful Authentication Response : redirect_uri?code=abc&state=123 if (configuration.isRedirectToOriginalResource() && !isOnOriginalURL(request, response)) { return httpContext.redirect(getOriginalRedirectUrl(request, response)); } // (3b) original_uri?code=abc&state=123 or redirect_uri?code=abc&state=123 return validateAuthorizationCode(httpContext); } return httpContext.doNothing(); } private boolean isOnOriginalURL(HttpServletRequest request, HttpServletResponse response) { Optional optionalOrginalUrl = HttpStorageController.getInstance(configuration, request, response) .getAsString(ORIGINAL_REQUEST); if (optionalOrginalUrl.isEmpty()) { // If no original url, return true so we don't redirect. return true; } String originalUrl = optionalOrginalUrl.get(); if (originalUrl.contains("?")) { originalUrl = originalUrl.substring(0, originalUrl.indexOf('?')); } return request.getRequestURL().toString().equals(originalUrl); } private String getOriginalRedirectUrl(HttpServletRequest request, HttpServletResponse response) { return getOriginalUrl(request, response) + "?" + request.getQueryString(); } private String getOriginalUrl(HttpServletRequest request, HttpServletResponse response) { String originalUrl = HttpStorageController.getInstance(configuration, request, response) .getAsString(ORIGINAL_REQUEST) .get(); // checked before if (originalUrl.contains("?")) { originalUrl = originalUrl.substring(0, originalUrl.indexOf('?')); } return originalUrl; } private RequestData getRequestData(HttpServletRequest request, HttpServletResponse response) { String requestJson = HttpStorageController.getInstance(configuration, request, response) .getAsString(ORIGINAL_REQUEST_DATA_JSON) .get(); return RequestData.of(requestJson); } /** * (3) & (4-6) An Authorization Code returned to Client (RP) via * Authorization Code Flow must be validated and exchanged for an ID Token, * an Access Token and optionally a Refresh Token directly. * * @param httpContext the {@link HttpMessageContext} to validate * authorization code from * @return the authentication status. */ private AuthenticationStatus validateAuthorizationCode(HttpMessageContext httpContext) { HttpServletRequest request = httpContext.getRequest(); HttpServletResponse response = httpContext.getResponse(); String error = request.getParameter(ERROR_PARAM); String errorDescription = request.getParameter(ERROR_DESCRIPTION_PARAM); if (!isEmpty(error)) { // Error responses sent to the redirect_uri LOGGER.log(WARNING, "Error occurred in receiving Authorization Code : {0} caused by {1}", new Object[]{error, errorDescription}); return httpContext.notifyContainerAboutLogin(INVALID_RESULT); } stateController.remove(request, response); LOGGER.finer("Authorization Code received, now fetching Access token & Id token"); Response tokenResponse = tokenController.getTokens(request); JsonObject tokensObject = readJsonObject(tokenResponse.readEntity(String.class)); if (tokenResponse.getStatus() == OK.getStatusCode()) { // Successful Token Response updateContext(tokensObject); OpenIdCredential credential = new OpenIdCredential(tokensObject, httpContext, configuration.getTokenMinValidity()); CredentialValidationResult validationResult = identityStoreHandler.validate(credential); // Register session manually (if @AutoApplySession used, this would be done by its interceptor) httpContext.setRegisterSession(validationResult.getCallerPrincipal().getName(), validationResult.getCallerGroups()); if (configuration.isRedirectToOriginalResource()) { // Restore request manually (if @LoginToContinue used, this would be done by its interceptor) httpContext.withRequest(new HttpServletRequestDelegator(request, getRequestData(request, response))); } return httpContext.notifyContainerAboutLogin(validationResult); } // Token Request is invalid or unauthorized LOGGER.log(WARNING, "Error occurred in validating Authorization Code : {0} caused by {1}", new Object[] { tokensObject.getString(ERROR_PARAM, "Unknown Error"), tokensObject.getString(ERROR_DESCRIPTION_PARAM, "Unknown") }); return httpContext.notifyContainerAboutLogin(INVALID_RESULT); } private AuthenticationStatus reAuthenticate(HttpMessageContext httpContext) throws AuthenticationException { HttpServletRequest request = httpContext.getRequest(); HttpServletResponse response = httpContext.getResponse(); synchronized (getSessionLock(httpContext.getRequest())) { boolean accessTokenExpired = context.getAccessToken().isExpired(); boolean identityTokenExpired = context.getIdentityToken().isExpired(); if (accessTokenExpired || identityTokenExpired) { if (accessTokenExpired) { LOGGER.fine("Access Token is expired. Request new Access Token with Refresh Token."); } if (identityTokenExpired) { LOGGER.fine("Identity Token is expired. Request new Identity Token with Refresh Token."); } AuthenticationStatus refreshStatus = context.getRefreshToken() .map(rt -> this.refreshTokens(httpContext, rt)) .orElse(SEND_FAILURE); if (refreshStatus != SUCCESS) { LOGGER.log(FINE, "Failed to refresh token (Refresh Token might be invalid)."); logout(request, response); } return refreshStatus; } } return SUCCESS; } private AuthenticationStatus refreshTokens(HttpMessageContext httpContext, RefreshToken refreshToken) { Response response = tokenController.refreshTokens(refreshToken); JsonObject tokensObject = readJsonObject(response.readEntity(String.class)); if (response.getStatus() == Response.Status.OK.getStatusCode()) { // Successful Token Response updateContext(tokensObject); OpenIdCredential credential = new OpenIdCredential(tokensObject, httpContext, configuration.getTokenMinValidity()); CredentialValidationResult validationResult = identityStoreHandler.validate(credential); // Do not register session, as this will invalidate the currently active session (destroys session beans and removes attributes set in session)! // httpContext.setRegisterSession(validationResult.getCallerPrincipal().getName(), validationResult.getCallerGroups()); return httpContext.notifyContainerAboutLogin(validationResult); } // Token Request is invalid (refresh token invalid or expired) String error = tokensObject.getString(ERROR_PARAM, "Unknown Error"); String errorDescription = tokensObject.getString(ERROR_DESCRIPTION_PARAM, "Unknown"); LOGGER.log(FINE, "Error occurred in refreshing Access Token and Refresh Token : {0} caused by {1}", new Object[]{error, errorDescription}); return SEND_FAILURE; } private void logout(HttpServletRequest request, HttpServletResponse response) { LogoutConfiguration logout = configuration.getLogoutConfiguration(); if (logout == null) { LOGGER.log(WARNING, "Logout invoked on session without OpenID session"); redirect(response, request.getContextPath()); return; } HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } /* * See section 5. RP-Initiated Logout * https://openid.net/specs/openid-connect-session-1_0.html#RPLogout */ if (logout.isNotifyProvider() && !isEmpty(configuration.getProviderMetadata().getEndSessionEndpoint())) { UriBuilder logoutURI = UriBuilder.fromUri(configuration.getProviderMetadata().getEndSessionEndpoint()) .queryParam( ID_TOKEN_HINT, context.getIdentityToken().getToken()); if (!isEmpty(logout.getRedirectURI())) { // User Agent redirected to POST_LOGOUT_REDIRECT_URI after a logout operation performed in OP. logoutURI.queryParam(POST_LOGOUT_REDIRECT_URI, logout.buildRedirectURI(request)); } redirect(response, logoutURI.toString()); } else if (!isEmpty(logout.getRedirectURI())) { redirect(response, logout.buildRedirectURI(request)); } else { // Redirect user to OpenID connect provider for re-authentication authenticationController.authenticateUser(request, response); } } private static void redirect(HttpServletResponse response, String uri) { try { response.sendRedirect(uri); } catch (IOException e) { throw new IllegalStateException(e); } } private JsonObject readJsonObject(String tokensBody) { LOGGER.fine("tokensBody = " + tokensBody); try (JsonReader reader = Json.createReader(new StringReader(tokensBody))) { return reader.readObject(); } } private void updateContext(JsonObject tokensObject) { context.setTokenType(tokensObject.getString(TOKEN_TYPE, null)); String refreshToken = tokensObject.getString(REFRESH_TOKEN, null); if (nonNull(refreshToken)) { context.setRefreshToken(new RefreshTokenImpl(refreshToken)); } JsonNumber expiresIn = tokensObject.getJsonNumber(EXPIRES_IN); if (nonNull(expiresIn)) { context.setExpiresIn(expiresIn.longValue()); } } private Object getSessionLock(HttpServletRequest request) { HttpSession session = request.getSession(); Object lock = session.getAttribute(SESSION_LOCK_NAME); if (isNull(lock)) { synchronized (OpenIdAuthenticationMechanism.class) { lock = session.getAttribute(SESSION_LOCK_NAME); if (isNull(lock)) { lock = new Lock(); session.setAttribute(SESSION_LOCK_NAME, lock); } } } return lock; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy