/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ package org.apache.nifi.web.security.jwt; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.SigningKeyResolverAdapter; import io.jsonwebtoken.UnsupportedJwtException; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.admin.service.AdministrationException; import org.apache.nifi.admin.service.KeyService; import org.apache.nifi.key.Key; import org.apache.nifi.web.security.token.LoginAuthenticationToken; import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; import java.util.Calendar; /** * */ public class JwtService { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JwtService.class); private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256; private static final String KEY_ID_CLAIM = "kid"; private static final String USERNAME_CLAIM = "preferred_username"; private final KeyService keyService; public JwtService(final KeyService keyService) { this.keyService = keyService; } public String getAuthenticationFromToken(final String base64EncodedToken) throws JwtException { // The library representations of the JWT should be kept internal to this service. try { final Jws<Claims> jws = parseTokenFromBase64EncodedString(base64EncodedToken); if (jws == null) { throw new JwtException("Unable to parse token"); } // Additional validation that subject is present if (StringUtils.isEmpty(jws.getBody().getSubject())) { throw new JwtException("No subject available in token"); } // TODO: Validate issuer against active registry? if (StringUtils.isEmpty(jws.getBody().getIssuer())) { throw new JwtException("No issuer available in token"); } return jws.getBody().getSubject(); } catch (JwtException e) { logger.debug("The Base64 encoded JWT: " + base64EncodedToken); final String errorMessage = "There was an error validating the JWT"; logger.error(errorMessage, e); throw e; } } private Jws<Claims> parseTokenFromBase64EncodedString(final String base64EncodedToken) throws JwtException { try { return Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() { @Override public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { final String identity = claims.getSubject(); // Get the key based on the key id in the claims final Integer keyId = claims.get(KEY_ID_CLAIM, Integer.class); final Key key = keyService.getKey(keyId); // Ensure we were able to find a key that was previously issued by this key service for this user if (key == null || key.getKey() == null) { throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]"); } return key.getKey().getBytes(StandardCharsets.UTF_8); } }).parseClaimsJws(base64EncodedToken); } catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) { // TODO: Exercise all exceptions to ensure none leak key material to logs final String errorMessage = "Unable to validate the access token."; throw new JwtException(errorMessage, e); } } /** * Generates a signed JWT token from the provided (Spring Security) login authentication token. * * @param authenticationToken an instance of the Spring Security token after login credentials have been verified against the respective information source * @return a signed JWT containing the user identity and the identity provider, Base64-encoded * @throws JwtException if there is a problem generating the signed token */ public String generateSignedToken(final LoginAuthenticationToken authenticationToken) throws JwtException { if (authenticationToken == null) { throw new IllegalArgumentException("Cannot generate a JWT for a null authentication token"); } // Set expiration from the token final Calendar expiration = Calendar.getInstance(); expiration.setTimeInMillis(authenticationToken.getExpiration()); final Object principal = authenticationToken.getPrincipal(); if (principal == null || StringUtils.isEmpty(principal.toString())) { final String errorMessage = "Cannot generate a JWT for a token with an empty identity issued by " + authenticationToken.getIssuer(); logger.error(errorMessage); throw new JwtException(errorMessage); } // Create a JWT with the specified authentication final String identity = principal.toString(); final String username = authenticationToken.getName(); try { // Get/create the key for this user final Key key = keyService.getOrCreateKey(identity); final byte[] keyBytes = key.getKey().getBytes(StandardCharsets.UTF_8); logger.trace("Generating JWT for " + authenticationToken); // TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens // Build the token return Jwts.builder().setSubject(identity) .setIssuer(authenticationToken.getIssuer()) .setAudience(authenticationToken.getIssuer()) .claim(USERNAME_CLAIM, username) .claim(KEY_ID_CLAIM, key.getId()) .setExpiration(expiration.getTime()) .setIssuedAt(Calendar.getInstance().getTime()) .signWith(SIGNATURE_ALGORITHM, keyBytes).compact(); } catch (NullPointerException | AdministrationException e) { final String errorMessage = "Could not retrieve the signing key for JWT for " + identity; logger.error(errorMessage, e); throw new JwtException(errorMessage, e); } } }