/src/main/java/org/integratedmodelling/node/service/AuthService.java
Java | 660 lines | 406 code | 89 blank | 165 comment | 44 complexity | a460502efb758a26a9a69e794b024095 MD5 | raw file
- /**
- * - BEGIN LICENSE: 4552165799761088680 -
- *
- * Copyright (C) 2014-2018 by:
- * - J. Luke Scott <luke@cron.works>
- * - Ferdinando Villa <ferdinando.villa@bc3research.org>
- * - any other authors listed in the @author annotations in source files
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the Affero General Public License
- * Version 3 or any later version.
- *
- * This program 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
- * Affero General Public License for more details.
- *
- * You should have received a copy of the Affero General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
- * The license is also available at: https://www.gnu.org/licenses/agpl.html
- *
- * - END LICENSE -
- */
- package org.integratedmodelling.node.service;
- import java.io.IOException;
- import java.lang.reflect.Constructor;
- import java.net.MalformedURLException;
- import java.time.ZonedDateTime;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Map.Entry;
- import javax.annotation.PostConstruct;
- import javax.mail.MessagingException;
- import org.apache.commons.lang3.RandomStringUtils;
- import org.hibernate.validator.internal.constraintvalidators.hv.EmailValidator;
- import org.integratedmodelling.node.auth.AuthenticationToken;
- import org.integratedmodelling.node.auth.Role;
- import org.integratedmodelling.node.auth.VerifyEmailClickbackToken;
- import org.integratedmodelling.node.config.CoreApplicationConfigProd;
- import org.integratedmodelling.node.config.WebSecurityConfig;
- import org.integratedmodelling.node.domain.ProfileResource;
- import org.integratedmodelling.node.domain.UserData;
- import org.integratedmodelling.node.domain.UserData.AccountStatus;
- import org.integratedmodelling.node.exception.AuthenticationFailedException;
- import org.integratedmodelling.node.exception.BadRequestException;
- import org.integratedmodelling.node.exception.JwksNotFoundException;
- import org.integratedmodelling.node.exception.JwtExpiredException;
- import org.integratedmodelling.node.exception.TokenGenerationException;
- import org.integratedmodelling.node.exception.UnauthorizedException;
- import org.integratedmodelling.node.exception.UserEmailExistsException;
- import org.integratedmodelling.node.exception.UserExistsException;
- import org.integratedmodelling.node.resource.token.ActivateAccountClickbackToken;
- import org.integratedmodelling.node.resource.token.ChangePasswordClickbackToken;
- import org.integratedmodelling.node.resource.token.ClickbackToken;
- import org.integratedmodelling.node.util.DateTimeUtil;
- import org.jose4j.jwk.HttpsJwks;
- import org.jose4j.jws.AlgorithmIdentifiers;
- import org.jose4j.jws.JsonWebSignature;
- import org.jose4j.jwt.JwtClaims;
- import org.jose4j.jwt.MalformedClaimException;
- import org.jose4j.jwt.NumericDate;
- import org.jose4j.jwt.consumer.InvalidJwtException;
- import org.jose4j.jwt.consumer.JwtConsumer;
- import org.jose4j.jwt.consumer.JwtConsumerBuilder;
- import org.jose4j.jwt.consumer.JwtContext;
- import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;
- import org.jose4j.lang.JoseException;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Profile;
- import org.springframework.scheduling.annotation.Scheduled;
- import org.springframework.security.authentication.AuthenticationManager;
- import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
- import org.springframework.security.core.Authentication;
- import org.springframework.security.core.AuthenticationException;
- import org.springframework.security.core.GrantedAuthority;
- import org.springframework.security.core.context.SecurityContextHolder;
- import org.springframework.security.core.userdetails.UsernameNotFoundException;
- import org.springframework.stereotype.Component;
- @Component
- @Profile({ CoreApplicationConfigProd.CONFIG_PROFILE })
- public class AuthService extends Service {
- private static final int ALLOWED_CLOCK_SKEW_MS = 30000;
- private static final String DEFAULT_TOKEN_CLASS = AuthenticationToken.class.getSimpleName();
- private static final long JWKS_UPDATE_INTERVAL_MS = 10 * 60 * 1000; // every 10 minutes
- private static final String JWT_CLAIM_KEY_PERMISSIONS = "perms";
- private static final String JWT_CLAIM_TOKEN_TYPE = "cls";
- private static final String JWT_DEFAULT_AUDIENCE = "all";
- @SuppressWarnings("unchecked")
- private static final Class<Collection<Role>> ROLE_COLLECTION_CLASS = (Class<Collection<Role>>) (Class<?>) Collection.class;
- @Autowired
- private AuthenticationManager authenticationManager;
- @Autowired
- private DateTimeUtil dateTimeUtil;
- @Autowired
- private EmailService emailManager;
- private EmailValidator emailValidator = new EmailValidator();
- protected Map<String, HttpsJwks> jwksUpdaters;
- private Map<String, JwtConsumer> jwksVerifiers;
- private JwtConsumer preValidationExtractor;
- @Autowired
- private KLabUserDetailsService userDetailsManager;
- @Autowired
- protected WebSecurityConfig webSecurityConfig;
- public void activateAccount() {
- // the user is already authenticated via clickback token header, but we want
- // to do some extra verification because they aren't supplying a password
- Authentication authentication = securityHelper.getLoggedInAuthentication();
- if (!(authentication instanceof ActivateAccountClickbackToken)) {
- throw new AuthenticationFailedException("The token submitted was not valid for activating an account.");
- }
- // this will also verify that the account started in pendingActivation
- userDetailsManager.activateUser(securityHelper.getLoggedInUsername());
- }
- // public UserData addUserToGroups(String username, List<String> groups) {
- // UserData userData = securityHelper.getUserData(username);
- // userData.addGroups(groups);
- // userDetailsManager.updateUser(userData);
- // return userData;
- // }
- public AuthenticationToken authenticate(String username, String password) {
- AuthenticationToken result = null;
- username = dataUtil.cleanseUsername(username);
- try {
- // NOTE: this is not created with createAuthenticationToken() because it's not saved to the DB
- Authentication authRequest = new UsernamePasswordAuthenticationToken(username, password);
- Authentication authResult = authenticationManager.authenticate(authRequest);
- logger.info(username, "Authenticated user via password authentication " + authResult.getAuthorities(),
- null);
- if (!authResult.isAuthenticated()) {
- String msg = "Something went wrong with authentication. Result.isAuthenticated() == false, but no exception was thrown.";
- logger.error(msg + " Throwing now...");
- throw new AuthenticationException(msg) {
- private static final long serialVersionUID = -6920183737495378174L;
- };
- } else {
- if (emailValidator.isValid(username, null)) {
- // if the user isn't in Mongo yet, but it appears that the username is an email address,
- // then it's OK to auto-create the user with username == email.
- // NOTE: This is valid only because they have successfully authenticated above (i.e.
- // they can be assumed to be "approved" by LDAP or whatever component allowed .authenticate())
- createMongoUserIfNecessary(username, username);
- }
- result = createAuthenticationToken(username, AuthenticationToken.class);
- // register the authentication with Spring Security
- SecurityContextHolder.getContext().setAuthentication(result);
- updateLastLogin(username);
- }
- } catch (AuthenticationException e) {
- String msg = "Login failed for user: " + username;
- logger.error(username, msg, e);
- throw new AuthenticationFailedException(msg, e);
- }
- return result;
- }
- // public AuthenticationToken authenticateWithCertFile(String certFileContent) throws IOException, PGPException {
- // Properties properties = licenseManager.readCertFileContent(certFileContent);
- // String username = properties.getProperty(LicenseService.Fields.USER);
- // logger.info(String.format("Attempting to authenticate user %s via cert file...", username));
- //
- // String expiryString = properties.getProperty(LicenseService.Fields.EXPIRY);
- //
- // DateTime expiry = DateTime.parse(expiryString);
- // if (expiry.isBeforeNow()) {
- // String msg = String.format("The cert file submitted for user %s is expired.", username);
- // logger.error(msg);
- // throw new AuthenticationFailedException(msg);
- // }
- //
- // logger.info(
- // String.format("Successfully authenticated user %s via cert file. Generating EngineToken...", username));
- //
- // AuthenticationToken result = createAuthenticationToken(username, EngineToken.class);
- // updateLastEngineConnection(username);
- // return result;
- // }
- /**
- * By default, we just create an instance of the jose4j /jwks retriever.
- *
- * Override this method to provide a mock object for unit tests, so that we don't require any network communication.
- */
- protected HttpsJwks buildJwksClient(String url) {
- return new HttpsJwks(url);
- }
- /**
- * The user is logged in via username/password
- */
- // public void changePassword(String oldPassword, String newPassword) {
- // String username = securityHelper.getLoggedInUsername();
- // UsernamePasswordAuthenticationToken usernamePassword = new UsernamePasswordAuthenticationToken(username,
- // oldPassword);
- // Authentication authResult = authenticationManager.authenticate(usernamePassword);
- // if (!authResult.isAuthenticated()) {
- // String msg = String.format("Old password was incorrect while trying password change for user '%s'.",
- // username);
- // logger.error(msg);
- // throw new AuthenticationFailedException(msg);
- // }
- //
- // setPasswordAndSendVerificationEmail(newPassword);
- // }
- /**
- * ALL tokens should be created with these two methods
- */
- public AuthenticationToken createAuthenticationToken(String username,
- Class<? extends AuthenticationToken> tokenType) {
- if (ClickbackToken.class.isAssignableFrom(tokenType)) {
- throw new TokenGenerationException(
- "ClickbackTokens must be generated by createClickbackToken(), not createAuthenticationToken()",
- null);
- }
- AuthenticationToken result = null;
- username = dataUtil.cleanseUsername(username);
- UserData userData = userDetailsManager.loadUserByUsername(username);
- Collection<Role> userAuthorities = userData.getAuthorities();
- Constructor<? extends AuthenticationToken> constructor = null;
- try {
- constructor = tokenType.getConstructor(String.class, String.class, ROLE_COLLECTION_CLASS);
- result = constructor.newInstance(coreApplicationConfig.getPartnerId(), username, userAuthorities);
- } catch (Exception e) {
- try {
- // probably a token type that doesn't accept roles in the constructor (cronjob/engine/etc).
- // re-try with just partner ID + username
- constructor = tokenType.getConstructor(String.class, String.class, ROLE_COLLECTION_CLASS);
- result = constructor.newInstance(coreApplicationConfig.getPartnerId(), username);
- } catch (Exception e2) {
- // shouldn't ever get here, but if we do, wrap in a non-checked Exception type
- throw new TokenGenerationException("Unable to get token constructor method.", e2);
- }
- }
- // In case the token type sets its own roles (i.e. no roles parameter in the constructor),
- // we need to double check the user has permissions to generate tokens with all included roles
- // (this is a no-op if the constructor accepted a roles parameter)
- for (GrantedAuthority authority : result.getAuthorities()) {
- if (!userAuthorities.contains(authority)) {
- throw new BadRequestException(String.format(
- "User '%s' does not have required role '%s' for this token type.", username, authority));
- }
- }
- result.setAuthenticated(true);
- result.setTokenString(generateJwtString(result));
- return result;
- }
- /**
- * ALL tokens should be created with these two methods
- */
- public ClickbackToken createClickbackToken(String username, Class<? extends ClickbackToken> tokenType)
- throws MalformedURLException {
- ClickbackToken result = null;
- username = dataUtil.cleanseUsername(username);
- try {
- result = tokenType.getConstructor(String.class, String.class)
- .newInstance(coreApplicationConfig.getPartnerId(), username);
- } catch (Exception e) {
- // mainly this is to throw RuntimeException instead of a checked exception
- // (shouldn't get here though)
- throw new TokenGenerationException("Unable to get token constructor method.", e);
- }
- result.setClickbackUrl(coreApplicationConfig);
- result.setAuthenticated(true);
- result.setTokenString(generateJwtString(result));
- // TODO save clickbacks? (payload too big for JWT?)
- // tokenRepository.save(result);
- return result;
- }
- private void createMongoUser(String username, String email) {
- UserData userData;
- userData = new UserData();
- userData.setUsername(username);
- userData.setEmail(email);
- userData.setRoles(Arrays.asList(Role.ROLE_USER));
- userData.setRegistrationDate();
- userDetailsManager.createMongoUser(userData);
- }
- private void createMongoUserIfNecessary(String username, String email) {
- try {
- createMongoUser(username, email);
- } catch (UserExistsException | UserEmailExistsException e) {
- // OK to continue silently
- }
- }
- public void createNewUser(String username, String email) throws TokenGenerationException, MalformedURLException {
- UserData userData = null;
- try {
- userData = userDetailsManager.loadUserByUsername(username);
- } catch (UsernameNotFoundException e) {
- }
- if (userData != null) {
- if (!AccountStatus.pendingActivation.equals(userData.getAccountStatus())
- || !email.equals(userData.getEmail())) {
- // an existing account is in Mongo with either a mis-matched email or is not in 'pendingActivation' state
- // so don't let the user re-register. (Active users without LDAP records can 'reset password' instead)
- throw new BadRequestException("An account with this username already exists.\n"
- + "If you are re-sending a registration email, please use the same email address as before.");
- }
- // the existing user account is pendingActivation and the email matches,
- // so it's OK to register as if it's a new account
- } else {
- // user is brand new
- try {
- createMongoUser(username, email);
- } catch (UserExistsException | UserEmailExistsException e) {
- throw new BadRequestException(e.getMessage(), e);
- }
- }
- ClickbackToken clickbackToken = createClickbackToken(username, ActivateAccountClickbackToken.class);
- emailManager.sendNewUser(email, username, clickbackToken.getClickbackUrl());
- }
- /**
- * email-verification-specific wrapper for createClickbackToken()
- */
- public VerifyEmailClickbackToken createVerifyEmailClickbackToken(String username, String email)
- throws MalformedURLException {
- VerifyEmailClickbackToken token = (VerifyEmailClickbackToken) createClickbackToken(username,
- VerifyEmailClickbackToken.class);
- token.setNewEmailAddress(email);
- // TODO need to save it somehow, I think, because this is too much data for JWT (right?)
- // tokenRepository.save(token);
- return token;
- }
- public void decorate(UserData userData) throws MalformedURLException {
- if (userData.getServerUrl() == null) {
- userData.setServerUrl(coreApplicationConfig.getEngineUrl().toExternalForm());
- }
- }
- private String generateJwtString(AuthenticationToken token) {
- if (!token.isAuthenticated()) {
- return null;
- }
- JwtClaims claims = new JwtClaims();
- String tokenClass = token.getClass().getSimpleName();
- if (!tokenClass.equals(DEFAULT_TOKEN_CLASS)) {
- claims.setStringClaim(JWT_CLAIM_TOKEN_TYPE, tokenClass);
- }
- claims.setIssuer(webSecurityConfig.getIssuer());
- claims.setAudience(Arrays.asList(JWT_DEFAULT_AUDIENCE));
- claims.setSubject(token.getUsername());
- ZonedDateTime expirationTimeUtc = dateTimeUtil.localToUtc(token.getExpiration());
- NumericDate expirationTimeNumericDate = NumericDate.fromSeconds(expirationTimeUtc.toEpochSecond());
- claims.setExpirationTime(expirationTimeNumericDate);
- claims.setIssuedAt(NumericDate.now());
- claims.setGeneratedJwtId();
- List<String> roleStrings = new ArrayList<>();
- for (Role role : token.getRoles()) {
- roleStrings.add(role.name());
- }
- claims.setStringListClaim(JWT_CLAIM_KEY_PERMISSIONS, roleStrings);
- JsonWebSignature jws = new JsonWebSignature();
- jws.setPayload(claims.toJson());
- jws.setKey(webSecurityConfig.getJwtPrivateKey());
- jws.setKeyIdHeaderValue(webSecurityConfig.getJwtPrivateKeyId());
- jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
- String result;
- try {
- result = jws.getCompactSerialization();
- } catch (JoseException e) {
- result = null;
- logger.error(String.format("Failed to generate JWT token string for user '%s': ", token.getUsername()), e);
- }
- return result;
- }
- public ClickbackToken handleClickbackToken(String tokenString) {
- // TODO need to save it somehow, I think, because this is too much data for JWT (right?)
- // ClickbackToken token = (ClickbackToken) tokenRepository.findByTokenString(tokenString);
- // if (token == null) {
- // throw new ForbiddenException("The token submitted could not be found.");
- // }
- // token.handleClickback(this);
- // return token;
- return null;
- }
- @PostConstruct
- public void postConstruct() throws JoseException, IOException {
- preValidationExtractor = new JwtConsumerBuilder().setSkipAllValidators().setDisableRequireSignature()
- .setSkipSignatureVerification().build();
- jwksUpdaters = new HashMap<>();
- jwksVerifiers = new HashMap<>();
- for (Entry<String, String> entry : webSecurityConfig.getJwksEndpointsByIssuer().entrySet()) {
- HttpsJwks jwks = buildJwksClient(entry.getValue());
- HttpsJwksVerificationKeyResolver jwksKeyResolver = new HttpsJwksVerificationKeyResolver(jwks);
- JwtConsumer jwtVerifier = new JwtConsumerBuilder().setSkipDefaultAudienceValidation() // audience == Client ID, which will be checked against the policy later.
- .setAllowedClockSkewInSeconds(ALLOWED_CLOCK_SKEW_MS / 1000)
- .setVerificationKeyResolver(jwksKeyResolver).build();
- jwksUpdaters.put(entry.getKey(), jwks);
- jwksVerifiers.put(entry.getKey(), jwtVerifier);
- }
- }
- public void sendPasswordResetEmail(String username, String email, boolean invalidateCurrentPassword)
- throws MessagingException, MalformedURLException {
- UserData user = userDetailsManager.loadUserByUsername(username);
- if (email == null || !email.equals(user.getEmail())) {
- throw new AuthenticationFailedException("Could not find a user with that username and email address.");
- }
- if (invalidateCurrentPassword) {
- userDetailsManager.changePasswordForUser(username, RandomStringUtils.randomAlphanumeric(15));
- }
- ClickbackToken token = createClickbackToken(username, ChangePasswordClickbackToken.class);
- emailManager.sendPasswordReset(email, token.getClickbackUrl());
- }
- public void setEmailWithTokenVerification(String newEmail) {
- // The user will have logged in via PasswordChangeClickbackToken or username/password.
- UserData persistedUser = securityHelper.getLoggedInUserData();
- if (persistedUser == null) {
- throw new BadRequestException("Could not find a user with the token that was submitted.");
- } else if (!AccountStatus.active.equals(persistedUser.getAccountStatus())) {
- throw new BadRequestException("An active user could not be found with the token that was submitted.");
- } else {
- // String oldEmail = persistedUser.getEmail();
- persistedUser.setEmail(newEmail);
- userDetailsManager.updateUser(persistedUser);
- // TODO
- // emailManager.sendEmailChangeConfirmation(oldEmail, persistedUser.getUsername());
- }
- }
- private void setPasswordAndSendVerificationEmail(String newPassword) {
- // The user will have logged in via PasswordChangeClickbackToken or username/password.
- UserData persistedUser = securityHelper.getLoggedInUserData();
- if (persistedUser == null) {
- throw new BadRequestException("Could not find a user with the token that was submitted.");
- } else if (!AccountStatus.active.equals(persistedUser.getAccountStatus())) {
- throw new BadRequestException("An active user could not be found with the token that was submitted.");
- } else {
- // in case the user is changing their password to solve a broken state (i.e. Mongo but no LDAP)
- // this will prevent the missing LDAP record from breaking the process
- if (!userDetailsManager.springUserExists(persistedUser.getUsername())) {
- userDetailsManager.createSpringUser(persistedUser);
- }
- userDetailsManager.changePassword(null, newPassword);
- // send an email notifying the user their password was changed
- // TODO
- // emailManager.sendPasswordChangeConfirmation(persistedUser.getEmail());
- }
- }
- /**
- * The user is logged in by ActivateAccountClickbackToken or ChangePasswordClickbackToken,
- * and their account should be 'active' by now.
- */
- public void setPasswordWithTokenVerification(String newPassword) {
- // the user is already authenticated via clickback token header, but we want
- // to do some extra verification because they aren't supplying an old password
- Authentication authentication = securityHelper.getLoggedInAuthentication();
- if (!(authentication instanceof ChangePasswordClickbackToken)) {
- throw new AuthenticationFailedException("The token submitted was not valid for setting a new password.");
- }
- setPasswordAndSendVerificationEmail(newPassword);
- // TODO save & delete clickback tokens, right?
- // manually delete the token because the normal clickback mechanism won't be doing it
- // deleteToken(((ChangePasswordClickbackToken) authentication).getTokenString());
- }
- @Scheduled(fixedDelay = JWKS_UPDATE_INTERVAL_MS)
- public void updateKeys() throws IOException {
- for (HttpsJwks jwks : jwksUpdaters.values()) {
- try {
- jwks.refresh();
- } catch (JoseException e) {
- logger.error("Got exception while refreshing JKW key set from " + jwks.getLocation(), e);
- }
- }
- }
- private void updateLastLogin(String username) {
- UserData userData = userDetailsManager.loadUserByUsername(username);
- userData.setLastLogin();
- userDetailsManager.updateUser(userData);
- }
- public ProfileResource updateProfile(ProfileResource profileResource) throws MalformedURLException {
- UserData loggedInUser = securityHelper.getLoggedInUserData();
- dataUtil.cleanse(profileResource);
- UserData userData;
- logger.info("Attempting to update profile for user: " + profileResource.username);
- if (loggedInUser.getUsername().equals(profileResource.username)) {
- if (!loggedInUser.isAdmin()) {
- // a normal user is modifying him/herself. Enforce security rules.
- profileResource.accountStatus = loggedInUser.getAccountStatus();
- profileResource.groups = new ArrayList<>(loggedInUser.getGroups());
- profileResource.roles = new ArrayList<>(loggedInUser.getAuthorities());
- }
- userData = loggedInUser;
- // email changes must be done via email-verify clickback
- String existingEmail = userData.getEmail();
- if (!profileResource.email.equals(existingEmail)) {
- VerifyEmailClickbackToken token = createVerifyEmailClickbackToken(profileResource.username,
- profileResource.email);
- emailManager.sendVerifyEmailClickback(existingEmail, token.getClickbackUrl());
- // don't actually change it yet
- profileResource.email = existingEmail;
- }
- } else {
- // the user is modifying someone else. Make sure that's ok.
- // this should be logged by Spring
- if (!loggedInUser.isAdmin()) {
- throw new UnauthorizedException(
- String.format("The currently logged in user does not have access to modify user '%s'.",
- profileResource.username));
- }
- // throws if not found, meaning that an existing record is required.
- // (because 'create' is a very special thing)
- userData = securityHelper.getUserData(profileResource.username);
- }
- // NOTE: at this point, we know that either:
- // - user is modifying him/herself and groups are NOT changing
- // - user is an admin modifying someone else and groups MAY BE changing.
- userData.setFromProfileResource(profileResource);
- userDetailsManager.updateUser(userData);
- // re-apply anything that may have resulted from setFromProfileResource() (like filtering/merging/etc)
- profileResource = objectMapper.convertValue(userData, ProfileResource.class);
- return profileResource;
- }
- /**
- * Given a JWT token that has previously been generated by a login event, validate its payload & signature.
- * If it passes all checks and its payload can be extracted properly, then return
- * an AuthenticationToken representing it.
- */
- public AuthenticationToken validateJwt(String token) {
- AuthenticationToken result = new AuthenticationToken("UNKNOWN", "UNKNOWN");
- try {
- // first extract the partnerId so that we know which public key to use for validating the signature
- JwtContext jwtContext = preValidationExtractor.process(token);
- String partnerId = jwtContext.getJwtClaims().getIssuer().trim();
- JwtConsumer jwtVerifier = jwksVerifiers.get(partnerId);
- if (jwtVerifier == null) {
- String msg = String.format("Couldn't find JWT verifier for partnerId %s. I only know about %s.",
- partnerId, jwksVerifiers.keySet().toString());
- Exception e = new JwksNotFoundException(msg);
- logger.error(msg, e);
- throw e;
- }
- JwtClaims claims = jwtVerifier.processToClaims(token);
- String username = claims.getSubject();
- List<String> roleStrings = claims.getStringListClaimValue(JWT_CLAIM_KEY_PERMISSIONS);
- List<Role> roles = new ArrayList<>();
- for (String role : roleStrings) {
- roles.add(Role.valueOf(role));
- }
- // didn't throw an exception, so token is valid. Update the result and validate claims.
- result = new AuthenticationToken(partnerId, username, roles);
- // TODO should we filter by audience somehow? Maybe for "modeling" vs. "web" operations?
- // Audience (aud) - The "aud" (audience) claim identifies the recipients that the JWT is intended for.
- // Each principal intended to process the JWT must identify itself with a value in the audience claim.
- // If the principal processing the claim does not identify itself with a value in the aud claim
- // when this claim is present, then the JWT must be rejected.
- claims.getAudience();
- // Expiration time (exp) - The "exp" (expiration time) claim identifies the expiration time
- // on or after which the JWT must not be accepted for processing.
- // The value should be in NumericDate[10][11] format.
- NumericDate expirationTime = claims.getExpirationTime();
- long now = System.currentTimeMillis();
- if (expirationTime.isBefore(NumericDate.fromMilliseconds(now - ALLOWED_CLOCK_SKEW_MS))) {
- throw new JwtExpiredException(claims);
- }
- long issuedAtUtcMs = claims.getIssuedAt().getValueInMillis();
- ZonedDateTime issuedAt = dateTimeUtil.utcToLocal(issuedAtUtcMs);
- result.setIssuedAt(issuedAt);
- // TODO if claims validate, then mark the result as authenticated
- result.setAuthenticated(true);
- } catch (MalformedClaimException e) {
- // TODO
- logger.error("WTF", e);
- } catch (InvalidJwtException e) {
- // TODO
- logger.error("WTF", e);
- } catch (Exception e) {
- // it was a JWT token, but some other exception happened.
- logger.error("WTF", e);
- }
- return result;
- }
- }