/* * Copyright (c) 2015 the original author or authors * * 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. */ package io.werval.modules.jose; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import io.werval.modules.jose.internal.Issuer; import io.werval.modules.metrics.Metrics; import org.jose4j.json.JsonUtil; import org.jose4j.jws.JsonWebSignature; /** * JSON Web Token. */ // See https://auth0.com/blog/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies/ public class JWT { public static final String HTTP_HEADER_CONFIG_KEY = "jose.jwt.http_header"; public static final String TOKEN_METADATA_KEY = "JWT"; public static final String CLAIMS_METADATA_KEY = "JWT-Claims"; public static final String CLAIM_ISSUER = "iss"; public static final String CLAIM_SUBJECT = "sub"; public static final String CLAIM_ROLES = "roles"; public static final String CLAIM_ISSUED_AT = "iat"; public static final String CLAIM_NOT_BEFORE = "nbf"; public static final String CLAIM_EXPIRATION = "exp"; public static final String METRIC_ISSUED_TOKENS = "io.werval.modules.jose.issued-tokens"; public static final String METRIC_VALIDATED_TOKENS = "io.werval.modules.jose.validated-tokens"; public static final String METRIC_RENEWED_TOKENS = "io.werval.modules.jose.renewed-tokens"; public static final String METRIC_TOKEN_ISSUANCE_ERRORS = "io.werval.modules.jose.token-issuance-errors"; public static final String METRIC_TOKEN_VALIDATION_ERRORS = "io.werval.modules.jose.token-validation-errors"; public static final String METRIC_TOKEN_RENEWAL_ERRORS = "io.werval.modules.jose.token-renewal-errors"; private final String defaultIssuer; private final Map<String, Issuer> issuers; private final Metrics metrics; /* package */ JWT( String defaultIssuer, Map<String, Issuer> issuers, Metrics metrics ) { this.defaultIssuer = defaultIssuer; this.issuers = issuers; this.metrics = metrics; } public String tokenForClaims( Map<String, Object> claims ) throws JoseException { return tokenForClaims( defaultIssuer, claims ); } public String tokenForClaims( String issuerId, Map<String, Object> claims ) throws JoseException { // Token issuer Issuer issuer = issuers.get( issuerId ); Objects.requireNonNull( issuer, "Unknown issuer: " + issuerId ); // Prepare claims Map<String, Object> actualClaims = new LinkedHashMap<>( claims ); actualClaims.put( CLAIM_ISSUER, issuer.dn() ); setTimeRelatedClaimsIfAbsent( issuer, actualClaims ); // Create token try { JsonWebSignature jws = new JsonWebSignature(); jws.setKey( issuer.key() ); jws.setKeyIdHeaderValue( issuer.keyId() ); jws.setAlgorithmHeaderValue( issuer.algorithm() ); jws.setPayload( JsonUtil.toJson( actualClaims ) ); String jwt = jws.getCompactSerialization(); if( metrics != null ) { metrics.metrics().meter( METRIC_ISSUED_TOKENS ).mark(); } return jwt; } catch( Exception ex ) { if( metrics != null ) { metrics.metrics().meter( METRIC_TOKEN_ISSUANCE_ERRORS ).mark(); } throw new JoseException( "Unable to issue JSON Web Token", ex ); } } public Map<String, Object> claimsOfToken( String token ) throws JoseException { return claimsOfToken( defaultIssuer, token ); } public Map<String, Object> claimsOfToken( String issuerId, String token ) throws JoseException { // Token issuer Issuer issuer = issuers.get( issuerId ); Objects.requireNonNull( issuer, "Unknown issuer: " + issuerId ); try { // Validate token signature JsonWebSignature jws = new JsonWebSignature(); jws.setCompactSerialization( token ); jws.setKey( issuer.key() ); if( !jws.verifySignature() ) { throw new JoseException( "JSON Web Token signature verification failed" ); } // Extract claims Map<String, Object> claims = JsonUtil.parseJson( jws.getPayload() ); // Validate token not-before/expiration ZoneId utc = ZoneId.of( "UTC" ); ZonedDateTime nowUtc = ZonedDateTime.now( utc ); if( claims.get( CLAIM_NOT_BEFORE ) != null && ZonedDateTime.ofInstant( Instant.ofEpochSecond( (Long) claims.get( CLAIM_NOT_BEFORE ) ), utc ) .isAfter( nowUtc ) ) { throw new JoseException( "JSON Web Token is not valid yet!" ); } if( claims.get( CLAIM_EXPIRATION ) != null && ZonedDateTime.ofInstant( Instant.ofEpochSecond( (Long) claims.get( CLAIM_EXPIRATION ) ), utc ) .isBefore( nowUtc ) ) { throw new JoseException( "JSON Web Token has expired!" ); } if( metrics != null ) { metrics.metrics().meter( METRIC_VALIDATED_TOKENS ).mark(); } // Claims return Collections.unmodifiableMap( claims ); } catch( Exception ex ) { if( metrics != null ) { metrics.metrics().meter( METRIC_TOKEN_VALIDATION_ERRORS ).mark(); } throw new JoseException( "JSON Web Token validation failed", ex ); } } public String renewToken( String token ) throws JoseException { // Token issuer Issuer issuer = issuers.get( defaultIssuer ); try { // Validate token signature JsonWebSignature jws = new JsonWebSignature(); jws.setCompactSerialization( token ); jws.setKey( issuer.key() ); if( !jws.verifySignature() ) { throw new JoseException( "JSON Web Token signature verification failed" ); } // Extract claims, remove all time related ones and set them afresh Map<String, Object> claims = JsonUtil.parseJson( jws.getPayload() ); clearTimeRelatedClaims( claims ); setTimeRelatedClaimsIfAbsent( issuer, claims ); // Create renewed token jws = new JsonWebSignature(); jws.setKey( issuer.key() ); jws.setKeyIdHeaderValue( issuer.keyId() ); jws.setAlgorithmHeaderValue( issuer.algorithm() ); jws.setPayload( JsonUtil.toJson( claims ) ); String jwt = jws.getCompactSerialization(); if( metrics != null ) { metrics.metrics().meter( METRIC_RENEWED_TOKENS ).mark(); } return jwt; } catch( Exception ex ) { if( metrics != null ) { metrics.metrics().meter( METRIC_TOKEN_RENEWAL_ERRORS ).mark(); } throw new JoseException( "JSON Web Token renewal failed", ex ); } } private void clearTimeRelatedClaims( Map<String, Object> claims ) { claims.remove( CLAIM_ISSUED_AT ); claims.remove( CLAIM_NOT_BEFORE ); claims.remove( CLAIM_EXPIRATION ); } private void setTimeRelatedClaimsIfAbsent( Issuer issuer, Map<String, Object> claims ) { ZonedDateTime nowUtc = ZonedDateTime.now( ZoneId.of( "UTC" ) ); // Issued at if( claims.get( CLAIM_ISSUED_AT ) == null ) { claims.put( CLAIM_ISSUED_AT, nowUtc.toEpochSecond() ); } // Not before if( claims.get( CLAIM_NOT_BEFORE ) == null && issuer.notBeforeSeconds().isPresent() ) { ZonedDateTime nbf = nowUtc.minusSeconds( issuer.notBeforeSeconds().get() ); claims.put( CLAIM_NOT_BEFORE, nbf.toEpochSecond() ); } // Expiration if( claims.get( CLAIM_EXPIRATION ) == null && issuer.expirationSeconds().isPresent() ) { ZonedDateTime exp = nowUtc.plusSeconds( issuer.expirationSeconds().get() ); claims.put( CLAIM_EXPIRATION, exp.toEpochSecond() ); } } }