package ca.uhn.fhir.rest.server.security;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.text.ParseException;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
import org.mitre.jwt.signer.service.impl.JWKSetCacheService;
import org.mitre.jwt.signer.service.impl.SymmetricCacheService;
import org.mitre.oauth2.model.RegisteredClient;
import org.mitre.openid.connect.client.service.ClientConfigurationService;
import org.mitre.openid.connect.client.service.ServerConfigurationService;
import org.mitre.openid.connect.config.ServerConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import ca.uhn.fhir.rest.method.OtherOperationTypeEnum;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
public class OpenIdConnectBearerTokenServerInterceptor extends InterceptorAdapter {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OpenIdConnectBearerTokenServerInterceptor.class);
@Autowired
private ClientConfigurationService myClientConfigurationService;
@Autowired
private ServerConfigurationService myServerConfigurationService;
private int myTimeSkewAllowance = 300;
private SymmetricCacheService mySymmetricCacheService;
private JWKSetCacheService myValidationServices;
public OpenIdConnectBearerTokenServerInterceptor() {
mySymmetricCacheService = new SymmetricCacheService();
myValidationServices = new JWKSetCacheService();
}
@Override
public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException {
if (theRequestDetails.getOtherOperationType() == OtherOperationTypeEnum.METADATA) {
return true;
}
authenticate(theRequest);
return true;
}
public void authenticate(HttpServletRequest theRequest) throws AuthenticationException {
String token = theRequest.getHeader(Constants.HEADER_AUTHORIZATION);
if (token == null) {
throw new AuthenticationException("Not authorized (no authorization header found in request)");
}
if (!token.startsWith(Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER)) {
throw new AuthenticationException("Not authorized (authorization header does not contain a bearer token)");
}
token = token.substring(Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER.length());
SignedJWT idToken;
try {
idToken = SignedJWT.parse(token);
} catch (ParseException e) {
throw new AuthenticationException("Not authorized (bearer token could not be validated)", e);
}
// validate our ID Token over a number of tests
ReadOnlyJWTClaimsSet idClaims;
try {
idClaims = idToken.getJWTClaimsSet();
} catch (ParseException e) {
throw new AuthenticationException("Not authorized (bearer token could not be validated)", e);
}
String issuer = idClaims.getIssuer();
ServerConfiguration serverConfig = myServerConfigurationService.getServerConfiguration(issuer);
if (serverConfig == null) {
ourLog.error("No server configuration found for issuer: " + issuer);
throw new AuthenticationException("Not authorized (no server configuration found for issuer " + issuer + ")");
}
RegisteredClient clientConfig = myClientConfigurationService.getClientConfiguration(serverConfig);
if (clientConfig == null) {
ourLog.error("No client configuration found for issuer: " + issuer);
throw new AuthenticationException("Not authorized (no client configuration found for issuer " + issuer + ")");
}
// check the signature
JwtSigningAndValidationService jwtValidator = null;
JWSAlgorithm alg = idToken.getHeader().getAlgorithm();
if (alg.equals(JWSAlgorithm.HS256) || alg.equals(JWSAlgorithm.HS384) || alg.equals(JWSAlgorithm.HS512)) {
// generate one based on client secret
jwtValidator = mySymmetricCacheService.getSymmetricValidtor(clientConfig.getClient());
} else {
// otherwise load from the server's public key
jwtValidator = myValidationServices.getValidator(serverConfig.getJwksUri());
}
if (jwtValidator != null) {
if (!jwtValidator.validateSignature(idToken)) {
throw new AuthenticationException("Not authorized (signature validation failed)");
}
} else {
ourLog.error("No validation service found. Skipping signature validation");
throw new AuthenticationException("Not authorized (can't determine signature validator)");
}
// check expiration
if (idClaims.getExpirationTime() == null) {
throw new AuthenticationException("Id Token does not have required expiration claim");
} else {
// it's not null, see if it's expired
Date minAllowableExpirationTime = new Date(System.currentTimeMillis() - (myTimeSkewAllowance * 1000L));
Date expirationTime = idClaims.getExpirationTime();
if (!expirationTime.after(minAllowableExpirationTime)) {
throw new AuthenticationException("Id Token is expired: " + idClaims.getExpirationTime());
}
}
// check not before
if (idClaims.getNotBeforeTime() != null) {
Date now = new Date(System.currentTimeMillis() + (myTimeSkewAllowance * 1000));
if (now.before(idClaims.getNotBeforeTime())) {
throw new AuthenticationException("Id Token not valid untill: " + idClaims.getNotBeforeTime());
}
}
// check issued at
if (idClaims.getIssueTime() == null) {
throw new AuthenticationException("Id Token does not have required issued-at claim");
} else {
// since it's not null, see if it was issued in the future
Date now = new Date(System.currentTimeMillis() + (myTimeSkewAllowance * 1000));
if (now.before(idClaims.getIssueTime())) {
throw new AuthenticationException("Id Token was issued in the future: " + idClaims.getIssueTime());
}
}
}
public int getTimeSkewAllowance() {
return myTimeSkewAllowance;
}
public void setClientConfigurationService(ClientConfigurationService theClientConfigurationService) {
myClientConfigurationService = theClientConfigurationService;
}
public void setServerConfigurationService(ServerConfigurationService theServerConfigurationService) {
myServerConfigurationService = theServerConfigurationService;
}
public void setTimeSkewAllowance(int theTimeSkewAllowance) {
myTimeSkewAllowance = theTimeSkewAllowance;
}
public void setValidationServices(JWKSetCacheService theValidationServices) {
myValidationServices = theValidationServices;
}
}