/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.portal.security.sso.openid.connect.internal;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.theme.ThemeDisplay;
import com.liferay.portal.kernel.util.ListUtil;
import com.liferay.portal.kernel.util.Portal;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.Time;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.kernel.util.WebKeys;
import com.liferay.portal.security.sso.openid.connect.OpenIdConnectProvider;
import com.liferay.portal.security.sso.openid.connect.OpenIdConnectProviderMetadataFactory;
import com.liferay.portal.security.sso.openid.connect.OpenIdConnectProviderRegistry;
import com.liferay.portal.security.sso.openid.connect.OpenIdConnectServiceException;
import com.liferay.portal.security.sso.openid.connect.OpenIdConnectServiceHandler;
import com.liferay.portal.security.sso.openid.connect.OpenIdConnectSession;
import com.liferay.portal.security.sso.openid.connect.OpenIdConnectUserInfoProcessor;
import com.liferay.portal.security.sso.openid.connect.constants.OpenIdConnectWebKeys;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.ErrorObject;
import com.nimbusds.oauth2.sdk.GeneralException;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
import com.nimbusds.oauth2.sdk.TokenRequest;
import com.nimbusds.oauth2.sdk.TokenResponse;
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.oauth2.sdk.token.Tokens;
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
import com.nimbusds.openid.connect.sdk.Nonce;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* @author Thuong Dinh
* @author Edward C. Han
*/
@Component(immediate = true, service = OpenIdConnectServiceHandler.class)
public class OpenIdConnectServiceHandlerImpl
implements OpenIdConnectServiceHandler {
@Override
public boolean hasValidOpenIdConnectSession(HttpSession httpSession)
throws PortalException {
OpenIdConnectSession openIdConnectSession =
(OpenIdConnectSession)httpSession.getAttribute(
OpenIdConnectWebKeys.OPEN_ID_CONNECT_SESSION);
if (!hasValidAccessToken(openIdConnectSession)) {
return refreshAuthToken(openIdConnectSession);
}
return true;
}
@Override
public String processAuthenticationResponse(
ThemeDisplay themeDisplay, HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse)
throws PortalException {
try {
AuthenticationResponse authenticationResponse =
AuthenticationResponseParser.parse(
new URI(themeDisplay.getURLCurrent()));
if (authenticationResponse instanceof AuthenticationErrorResponse) {
AuthenticationErrorResponse authenticationerrorresponse =
(AuthenticationErrorResponse)authenticationResponse;
ErrorObject errorObject =
authenticationerrorresponse.getErrorObject();
throw new OpenIdConnectServiceException.
AuthenticationErrorException(errorObject.toString());
}
HttpSession httpSession = httpServletRequest.getSession();
OpenIdConnectSession openIdConnectSession =
(OpenIdConnectSession)httpSession.getAttribute(
OpenIdConnectWebKeys.OPEN_ID_CONNECT_SESSION);
if (Validator.isNull(openIdConnectSession)) {
throw new OpenIdConnectServiceException.
AuthenticationErrorException(
"No existing OpenId Connect session found");
}
AuthenticationSuccessResponse authenticationSuccessResponse =
(AuthenticationSuccessResponse)authenticationResponse;
validateState(
openIdConnectSession.getState(),
authenticationSuccessResponse.getState());
String openIdConnectProviderName =
openIdConnectSession.getOpenIdProviderName();
OpenIdConnectProvider openIdConnectProvider =
_openIdConnectProviderRegistry.getOpenIdConnectProvider(
openIdConnectProviderName);
if (openIdConnectProvider == null) {
throw new OpenIdConnectServiceException.ProviderException(
"Unable to get OpenId Connect provider with name " +
openIdConnectProviderName);
}
OpenIdConnectProviderMetadataFactory
openIdConnectProviderMetadataFactory =
openIdConnectProvider.
getOpenIdConnectProviderMetadataFactory();
OIDCProviderMetadata oidcProviderMetadata =
openIdConnectProviderMetadataFactory.getOIDCProviderMetadata();
OIDCClientInformation oidcClientInformation =
getOIDCClientInformation(
openIdConnectProvider, oidcProviderMetadata);
AuthorizationCode authorizationCode =
authenticationSuccessResponse.getAuthorizationCode();
OIDCTokenResponse oidcTokenResponse = requestIdToken(
oidcClientInformation, httpServletRequest, authorizationCode,
oidcProviderMetadata);
validateIdToken(
oidcClientInformation, openIdConnectSession.getNonce(),
oidcProviderMetadata, oidcTokenResponse);
Tokens tokens = oidcTokenResponse.getTokens();
UserInfo userInfo = requestUserInfo(
tokens.getAccessToken(), oidcProviderMetadata);
long userId = _openIdConnectUserInfoProcessor.processUserInfo(
userInfo, themeDisplay.getCompanyId());
httpSession.setAttribute(
OpenIdConnectWebKeys.OPEN_ID_CONNECT_LOGIN, userId);
openIdConnectSession.setUserInfo(userInfo);
updateSession(
openIdConnectSession, tokens, System.currentTimeMillis());
return null;
}
catch (BadJOSEException | GeneralException | IOException |
JOSEException | URISyntaxException e) {
if (_log.isWarnEnabled()) {
_log.warn(e, e);
}
throw new SystemException(e);
}
}
@Override
public void requestAuthentication(
String openIdConnectProviderName,
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse)
throws PortalException {
OpenIdConnectProvider openIdConnectProvider =
_openIdConnectProviderRegistry.getOpenIdConnectProvider(
openIdConnectProviderName);
if (openIdConnectProvider == null) {
throw new SystemException(
"Unable to get OpenId Connect provider with name " +
openIdConnectProviderName);
}
try {
State state = new State();
Nonce nonce = new Nonce();
Scope scope = Scope.parse(openIdConnectProvider.getScopes());
OpenIdConnectProviderMetadataFactory
openIdConnectProviderMetadataFactory =
openIdConnectProvider.
getOpenIdConnectProviderMetadataFactory();
OIDCProviderMetadata oidcProviderMetadata =
openIdConnectProviderMetadataFactory.getOIDCProviderMetadata();
OIDCClientInformation oidcClientInformation =
getOIDCClientInformation(
openIdConnectProvider, oidcProviderMetadata);
AuthenticationRequest authenticationRequest =
new AuthenticationRequest(
oidcProviderMetadata.getAuthorizationEndpointURI(),
new ResponseType(ResponseType.Value.CODE), scope,
oidcClientInformation.getID(),
createRedirectURI(httpServletRequest), state, nonce);
URI authenticationRequestURI = authenticationRequest.toURI();
httpServletResponse.sendRedirect(
authenticationRequestURI.toString());
HttpSession httpSession = httpServletRequest.getSession();
OpenIdConnectSession openIdConnectSession =
new OpenIdConnectSession(
openIdConnectProviderName, nonce, state);
httpSession.setAttribute(
OpenIdConnectWebKeys.OPEN_ID_CONNECT_SESSION,
openIdConnectSession);
}
catch (IOException | URISyntaxException e) {
throw new SystemException(
"Unable to communicate with OpenId Connect provider", e);
}
}
protected URI createRedirectURI(HttpServletRequest httpServletRequest)
throws PortalException, URISyntaxException {
StringBundler sb = new StringBundler(5);
ThemeDisplay themeDisplay =
(ThemeDisplay)httpServletRequest.getAttribute(
WebKeys.THEME_DISPLAY);
sb.append(
_portal.getLayoutFriendlyURL(
themeDisplay.getLayout(), themeDisplay));
sb.append(StringPool.SLASH);
sb.append(StringPool.DASH);
sb.append(OpenIdConnectWebKeys.OPEN_ID_CONNECT_RESPONSE_ACTION_NAME);
URI uri = new URI(sb.toString());
return uri;
}
protected OIDCClientInformation getOIDCClientInformation(
OpenIdConnectProvider openIdConnectProvider,
OIDCProviderMetadata oidcProviderMetadata)
throws OpenIdConnectServiceException {
ClientID clientID = new ClientID(openIdConnectProvider.getClientId());
OIDCClientMetadata oidcClientMetadata = new OIDCClientMetadata();
List<JWEAlgorithm> jweAlgorithms =
oidcProviderMetadata.getIDTokenJWEAlgs();
if (ListUtil.isNotEmpty(jweAlgorithms)) {
oidcClientMetadata.setIDTokenJWEAlg(jweAlgorithms.get(0));
}
List<JWSAlgorithm> jwsAlgorithms =
oidcProviderMetadata.getIDTokenJWSAlgs();
if (ListUtil.isNotEmpty(jwsAlgorithms)) {
oidcClientMetadata.setIDTokenJWSAlg(jwsAlgorithms.get(0));
}
oidcClientMetadata.setJWKSetURI(oidcProviderMetadata.getJWKSetURI());
Secret secret = new Secret(openIdConnectProvider.getClientSecret());
return new OIDCClientInformation(
clientID, new Date(), oidcClientMetadata, secret);
}
protected boolean hasValidAccessToken(
OpenIdConnectSession openIdConnectSession) {
AccessToken accessToken = openIdConnectSession.getAccessToken();
if (Validator.isNull(accessToken)) {
return false;
}
long currentTime = System.currentTimeMillis();
long lifetime = accessToken.getLifetime() * Time.SECOND;
long loginTime = openIdConnectSession.getLoginTime();
if ((currentTime - loginTime) < lifetime) {
return true;
}
return false;
}
protected boolean refreshAuthToken(
OpenIdConnectSession openIdConnectSession)
throws OpenIdConnectServiceException {
synchronized (openIdConnectSession) {
if (hasValidAccessToken(openIdConnectSession)) {
return true;
}
if (_log.isInfoEnabled()) {
_log.info(
"User session auth token is invalid, attempting to use " +
"refresh token to obtain a valid auth token");
}
RefreshToken refreshToken = openIdConnectSession.getRefreshToken();
if (refreshToken == null) {
if (_log.isInfoEnabled()) {
_log.info(
"Unable to refresh auth token because no refresh " +
"token is supplied");
}
return false;
}
String openIdConnectProviderName =
openIdConnectSession.getOpenIdProviderName();
OpenIdConnectProvider openIdConnectProvider =
_openIdConnectProviderRegistry.getOpenIdConnectProvider(
openIdConnectProviderName);
if (openIdConnectProvider == null) {
throw new OpenIdConnectServiceException.ProviderException(
"Unable to get OpenId Connect provider with name " +
openIdConnectProviderName);
}
OpenIdConnectProviderMetadataFactory
openIdConnectProviderMetadataFactory =
openIdConnectProvider.
getOpenIdConnectProviderMetadataFactory();
OIDCProviderMetadata oidcProviderMetadata =
openIdConnectProviderMetadataFactory.getOIDCProviderMetadata();
OIDCTokenResponse oidcTokenResponse = requestRefreshToken(
refreshToken, openIdConnectProvider, oidcProviderMetadata);
Tokens tokens = oidcTokenResponse.getOIDCTokens();
updateSession(
openIdConnectSession, tokens, System.currentTimeMillis());
return true;
}
}
protected OIDCTokenResponse requestIdToken(
OIDCClientInformation oidcClientInformation,
HttpServletRequest httpServletRequest,
AuthorizationCode authorizationCode,
OIDCProviderMetadata oidcProviderMetadata)
throws IOException, ParseException,
PortalException, URISyntaxException {
URI redirectURI = createRedirectURI(httpServletRequest);
TokenRequest tokenRequest = new TokenRequest(
oidcProviderMetadata.getTokenEndpointURI(),
new ClientSecretBasic(
oidcClientInformation.getID(),
oidcClientInformation.getSecret()),
new AuthorizationCodeGrant(authorizationCode, redirectURI));
HTTPRequest httpRequest = tokenRequest.toHTTPRequest();
HTTPResponse httpResponse = httpRequest.send();
TokenResponse tokenResponse = OIDCTokenResponseParser.parse(
httpResponse);
if (tokenResponse instanceof TokenErrorResponse) {
TokenErrorResponse tokenErrorResponse =
(TokenErrorResponse)tokenResponse;
ErrorObject errorObject = tokenErrorResponse.getErrorObject();
throw new OpenIdConnectServiceException.TokenErrorException(
errorObject.toString());
}
return (OIDCTokenResponse)tokenResponse;
}
protected OIDCTokenResponse requestRefreshToken(
RefreshToken refreshToken,
OpenIdConnectProvider openIdConnectProvider,
OIDCProviderMetadata oidcProviderMetadata)
throws OpenIdConnectServiceException {
try {
AuthorizationGrant refreshTokenGrant = new RefreshTokenGrant(
refreshToken);
ClientID clientID = new ClientID(
openIdConnectProvider.getClientId());
Secret clientSecret = new Secret(
openIdConnectProvider.getClientSecret());
ClientAuthentication clientAuthentication = new ClientSecretBasic(
clientID, clientSecret);
URI tokenEndpoint = oidcProviderMetadata.getTokenEndpointURI();
TokenRequest tokenRequest = new TokenRequest(
tokenEndpoint, clientAuthentication, refreshTokenGrant);
HTTPRequest httpRequest = tokenRequest.toHTTPRequest();
HTTPResponse httpResponse = httpRequest.send();
TokenResponse tokenResponse = OIDCTokenResponseParser.parse(
httpResponse);
if (tokenResponse instanceof TokenErrorResponse) {
TokenErrorResponse tokenErrorResponse =
(TokenErrorResponse)tokenResponse;
ErrorObject errorObject = tokenErrorResponse.getErrorObject();
throw new OpenIdConnectServiceException.TokenErrorException(
errorObject.toString());
}
return (OIDCTokenResponse)tokenResponse;
}
catch (IOException | ParseException e) {
throw new OpenIdConnectServiceException.
AuthenticationErrorException(
"Unable to process refresh request", e);
}
}
protected UserInfo requestUserInfo(
AccessToken accessToken, OIDCProviderMetadata oidcProviderMetadata)
throws IOException, ParseException, PortalException {
UserInfoRequest userInfoRequest = new UserInfoRequest(
oidcProviderMetadata.getUserInfoEndpointURI(),
(BearerAccessToken)accessToken);
HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
HTTPResponse httpResponse = httpRequest.send();
UserInfoResponse userInfoResponse = UserInfoResponse.parse(
httpResponse);
if (userInfoResponse instanceof UserInfoErrorResponse) {
UserInfoErrorResponse userInfoErrorResponse =
(UserInfoErrorResponse)userInfoResponse;
ErrorObject errorObject = userInfoErrorResponse.getErrorObject();
throw new OpenIdConnectServiceException.UserInfoErrorException(
errorObject.toString());
}
UserInfoSuccessResponse userInfoSuccessResponse =
(UserInfoSuccessResponse)userInfoResponse;
return userInfoSuccessResponse.getUserInfo();
}
protected void updateSession(
OpenIdConnectSession session, Tokens tokens, long loginTime) {
session.setAccessToken(tokens.getAccessToken());
session.setRefreshToken(tokens.getRefreshToken());
session.setLoginTime(loginTime);
}
protected IDTokenClaimsSet validateIdToken(
OIDCClientInformation oidcClientInformation, Nonce nonce,
OIDCProviderMetadata oidcProviderMetadata,
OIDCTokenResponse oidcTokenResponse)
throws BadJOSEException, GeneralException, JOSEException,
MalformedURLException, OpenIdConnectServiceException {
IDTokenValidator idTokenValidator = IDTokenValidator.create(
oidcProviderMetadata, oidcClientInformation, null);
OIDCTokens oidcTokens = oidcTokenResponse.getOIDCTokens();
return idTokenValidator.validate(oidcTokens.getIDToken(), nonce);
}
protected void validateState(State requestedState, State state)
throws OpenIdConnectServiceException {
if (!state.equals(requestedState)) {
throw new OpenIdConnectServiceException.
AuthenticationErrorException("Invalid state");
}
}
private static final Log _log = LogFactoryUtil.getLog(
OpenIdConnectServiceHandlerImpl.class);
@Reference
private OpenIdConnectProviderRegistry _openIdConnectProviderRegistry;
@Reference
private OpenIdConnectUserInfoProcessor _openIdConnectUserInfoProcessor;
@Reference
private Portal _portal;
}