001package org.apache.archiva.redback.authentication.jwt; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import io.jsonwebtoken.Claims; 023import io.jsonwebtoken.ExpiredJwtException; 024import io.jsonwebtoken.IncorrectClaimException; 025import io.jsonwebtoken.Jws; 026import io.jsonwebtoken.JwsHeader; 027import io.jsonwebtoken.JwtException; 028import io.jsonwebtoken.JwtParser; 029import io.jsonwebtoken.Jwts; 030import io.jsonwebtoken.MalformedJwtException; 031import io.jsonwebtoken.MissingClaimException; 032import io.jsonwebtoken.SignatureAlgorithm; 033import io.jsonwebtoken.SigningKeyResolverAdapter; 034import io.jsonwebtoken.UnsupportedJwtException; 035import io.jsonwebtoken.security.Keys; 036import io.jsonwebtoken.security.SignatureException; 037import org.apache.archiva.redback.authentication.AbstractAuthenticator; 038import org.apache.archiva.redback.authentication.AuthenticationDataSource; 039import org.apache.archiva.redback.authentication.AuthenticationException; 040import org.apache.archiva.redback.authentication.AuthenticationFailureCause; 041import org.apache.archiva.redback.authentication.AuthenticationResult; 042import org.apache.archiva.redback.authentication.Authenticator; 043import org.apache.archiva.redback.authentication.BearerTokenAuthenticationDataSource; 044import org.apache.archiva.redback.authentication.SimpleTokenData; 045import org.apache.archiva.redback.authentication.StringToken; 046import org.apache.archiva.redback.authentication.Token; 047import org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource; 048import org.apache.archiva.redback.authentication.TokenData; 049import org.apache.archiva.redback.authentication.TokenType; 050import org.apache.archiva.redback.configuration.UserConfiguration; 051import org.apache.archiva.redback.configuration.UserConfigurationKeys; 052import org.apache.commons.lang3.StringUtils; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055import org.springframework.stereotype.Service; 056 057import javax.annotation.PostConstruct; 058import javax.crypto.SecretKey; 059import javax.crypto.spec.SecretKeySpec; 060import javax.inject.Inject; 061import javax.inject.Named; 062import java.io.FileNotFoundException; 063import java.io.IOException; 064import java.io.InputStream; 065import java.io.OutputStream; 066import java.nio.file.Files; 067import java.nio.file.Path; 068import java.nio.file.Paths; 069import java.nio.file.attribute.PosixFilePermissions; 070import java.security.Key; 071import java.security.KeyFactory; 072import java.security.KeyPair; 073import java.security.NoSuchAlgorithmException; 074import java.security.PrivateKey; 075import java.security.PublicKey; 076import java.security.spec.InvalidKeySpecException; 077import java.security.spec.PKCS8EncodedKeySpec; 078import java.security.spec.X509EncodedKeySpec; 079import java.time.Duration; 080import java.time.Instant; 081import java.util.Arrays; 082import java.util.Base64; 083import java.util.Date; 084import java.util.HashMap; 085import java.util.LinkedHashMap; 086import java.util.Map; 087import java.util.Properties; 088import java.util.UUID; 089import java.util.concurrent.atomic.AtomicLong; 090import java.util.concurrent.locks.ReadWriteLock; 091import java.util.concurrent.locks.ReentrantReadWriteLock; 092 093import static org.apache.archiva.redback.configuration.UserConfigurationKeys.*; 094 095/** 096 * Authenticator for JWT tokens. This authenticator needs a secret key or keypair depending 097 * on the used algorithm for signing and verification. 098 * The key can be either volatile in memory, which means a new one is created, with each 099 * start of the service. Or it can be stored in a file. 100 * If this service is running in a cluster, you need a shared filesystem (NFS) for storing 101 * the key file otherwise different keys will be used in each instance. 102 * <p> 103 * You can renew the used key ({@link #renewSigningKey()}). The authenticator keeps a fixed 104 * sized list of the last keys used and stores the key identifier in the JWT header. 105 * <p> 106 * The default algorithm used for the JWT is currently {@link org.apache.archiva.redback.configuration.UserConfigurationKeys#AUTHENTICATION_JWT_SIGALG_ES384} 107 * 108 * If the <code>plainfile</code> keystore is used, only the most recent key is saved to the file. Not the 109 * complete list. 110 * 111 * The JWT tokens have a lifetime set (14400 seconds - 4 hours). 112 * 113 * The following configuration keys are used to setup this authenticator: 114 * <dl> 115 * <dt>{@value UserConfigurationKeys#AUTHENTICATION_JWT_KEYSTORETYPE}</dt> 116 * <dd>The type of the keystore, either <code>{@value UserConfigurationKeys#AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY}</code> 117 * (key is lost, if the jvm stops) or <code>{@value UserConfigurationKeys#AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE}</code></dd> 118 * <dt>{@value UserConfigurationKeys#AUTHENTICATION_JWT_SIGALG}</dt> 119 * <dd>The signature algorithm for the JWT. 120 * <ul> 121 * <li>HS256: HMAC using SHA-256</li> 122 * <li>HS384: HMAC using SHA-384</li> 123 * <li>HS512: HMAC using SHA-512</li> 124 * <li>ES256: ECDSA using P-256 and SHA-256</li> 125 * <li>ES384: ECDSA using P-384 and SHA-384</li> 126 * <li>ES512: ECDSA using P-521 and SHA-512</li> 127 * <li>RS256: RSASSA-PKCS-v1_5 using SHA-256</li> 128 * <li>RS384: RSASSA-PKCS-v1_5 using SHA-384</li> 129 * <li>RS512: RSASSA-PKCS-v1_5 using SHA-512</li> 130 * <li>PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256</li> 131 * <li>PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384</li> 132 * <li>PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512</li> 133 * </ul> 134 * </dd> 135 * <dt>{@value UserConfigurationKeys#AUTHENTICATION_JWT_MAX_KEYS}</dt> 136 * <dd>The maximum number of signature keys to keep in memory for verification</dd> 137 * <dt>{@value UserConfigurationKeys#AUTHENTICATION_JWT_KEYFILE}</dt> 138 * <dd>The key file. Either a full path to the file, or a single filename, which means it is stored in the working directory</dd> 139 * <dt>{@value UserConfigurationKeys#AUTHENTICATION_JWT_LIFETIME_MS}</dt> 140 * <dd>The default token lifetime in milliseconds</dd> 141 * </dl> 142 */ 143@Service( "authenticator#jwt" ) 144public class JwtAuthenticator extends AbstractAuthenticator implements Authenticator 145{ 146 private static final Logger log = LoggerFactory.getLogger( JwtAuthenticator.class ); 147 148 // 4 hours for standard tokens 149 public static final String DEFAULT_LIFETIME = "14400000"; 150 // 7 days for refresh tokens 151 public static final String DEFAULT_REFRESH_LIFETIME = "604800000"; 152 public static final String DEFAULT_KEYFILE = "jwt-key.xml"; 153 public static final String ID = "JwtAuthenticator"; 154 public static final String PROP_PRIV_ALG = "privateAlgorithm"; 155 public static final String PROP_PRIV_FORMAT = "privateFormat"; 156 public static final String PROP_PUB_ALG = "publicAlgorithm"; 157 public static final String PROP_PUB_FORMAT = "publicFormat"; 158 public static final String PROP_PRIVATEKEY = "privateKey"; 159 public static final String PROP_PUBLICKEY = "publicKey"; 160 public static final String PROP_KEYID = "keyId"; 161 private static final String ISSUER = "archiva.apache.org/redback"; 162 private static final String TOKEN_TYPE = "token_type"; 163 164 165 @Inject 166 @Named( value = "userConfiguration#default" ) 167 UserConfiguration userConfiguration; 168 169 boolean symmetricAlgorithm = true; 170 boolean fileStore = false; 171 LinkedHashMap<Long, SecretKey> secretKey; 172 LinkedHashMap<Long, KeyPair> keyPair; 173 String signatureAlgorithm; 174 String keystoreType; 175 Path keystoreFilePath; 176 int maxInMemoryKeys = 5; 177 AtomicLong keyCounter; 178 final SigningKeyResolver resolver = new SigningKeyResolver( ); 179 final ReadWriteLock lock = new ReentrantReadWriteLock( ); 180 private Duration tokenLifetime; 181 private Duration refreshTokenLifetime; 182 private Map<TokenType, JwtParser> parserMap = new HashMap<>( ); 183 184 185 private JwtParser getParser(TokenType type) { 186 return parserMap.get( type ); 187 } 188 public class SigningKeyResolver extends SigningKeyResolverAdapter 189 { 190 191 @Override 192 public Key resolveSigningKey( JwsHeader jwsHeader, Claims claims ) 193 { 194 Long keyId = Long.valueOf( jwsHeader.get( JwsHeader.KEY_ID ).toString() ); 195 Key key; 196 if (symmetricAlgorithm) { 197 key = getSecretKey( keyId ); 198 } else 199 { 200 KeyPair pair = getKeyPair( keyId ); 201 if (pair == null) { 202 throw new JwtKeyIdNotFoundException( "Key ID not found in current list. Verification failed." ); 203 } 204 key = pair.getPublic( ); 205 } 206 if (key==null) { 207 throw new JwtKeyIdNotFoundException( "Key ID not found in current list. Verification failed." ); 208 } 209 return key; 210 } 211 } 212 213 @Override 214 public String getId( ) 215 { 216 return ID; 217 } 218 219 @PostConstruct 220 public void init( ) throws AuthenticationException 221 { 222 super.initialize(); 223 this.keyCounter = new AtomicLong( System.currentTimeMillis( ) ); 224 this.keystoreType = userConfiguration.getString( AUTHENTICATION_JWT_KEYSTORETYPE, AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY ); 225 this.fileStore = this.keystoreType.equals( AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE ); 226 this.signatureAlgorithm = userConfiguration.getString( AUTHENTICATION_JWT_SIGALG, AUTHENTICATION_JWT_SIGALG_HS384 ); 227 this.maxInMemoryKeys = userConfiguration.getInt( AUTHENTICATION_JWT_MAX_KEYS, 5 ); 228 secretKey = new LinkedHashMap<Long, SecretKey>( ) 229 { 230 @Override 231 protected boolean removeEldestEntry( Map.Entry eldest ) 232 { 233 return size( ) > maxInMemoryKeys; 234 } 235 }; 236 keyPair = new LinkedHashMap<Long, KeyPair>( ) 237 { 238 @Override 239 protected boolean removeEldestEntry( Map.Entry eldest ) 240 { 241 return size( ) > maxInMemoryKeys; 242 } 243 }; 244 245 246 this.symmetricAlgorithm = this.signatureAlgorithm.startsWith( "HS" ); 247 248 if ( this.fileStore ) 249 { 250 String file = userConfiguration.getString( AUTHENTICATION_JWT_KEYFILE, DEFAULT_KEYFILE ); 251 this.keystoreFilePath = Paths.get( file ).toAbsolutePath( ); 252 handleKeyfile( ); 253 } 254 else 255 { 256 // In memory key store is the default 257 addNewKey( ); 258 } 259 this.parserMap.put(TokenType.ALL, Jwts.parserBuilder( ) 260 .setSigningKeyResolver( getResolver( ) ) 261 .requireIssuer( ISSUER ) 262 .build( )); 263 this.parserMap.put(TokenType.ACCESS_TOKEN, Jwts.parserBuilder( ) 264 .setSigningKeyResolver( getResolver( ) ) 265 .requireIssuer( ISSUER ) 266 .require( TOKEN_TYPE, TokenType.ACCESS_TOKEN.getClaim() ) 267 .build( )); 268 this.parserMap.put(TokenType.REFRESH_TOKEN, Jwts.parserBuilder( ) 269 .setSigningKeyResolver( getResolver( ) ) 270 .requireIssuer( ISSUER ) 271 .require( TOKEN_TYPE, TokenType.REFRESH_TOKEN.getClaim() ) 272 .build( )); 273 274 275 tokenLifetime = Duration.ofMillis( Long.parseLong( userConfiguration.getString( AUTHENTICATION_JWT_LIFETIME_MS, DEFAULT_LIFETIME ) ) ); 276 refreshTokenLifetime = Duration.ofMillis( Long.parseLong( userConfiguration.getString( AUTHENTICATION_JWT_REFRESH_LIFETIME_MS, DEFAULT_REFRESH_LIFETIME ) ) ); 277 } 278 279 private void addNewSecretKey( Long id, SecretKey key ) 280 { 281 lock.writeLock( ).lock( ); 282 try 283 { 284 this.secretKey.put( id, key ); 285 } 286 finally 287 { 288 lock.writeLock( ).unlock( ); 289 } 290 } 291 292 private void addNewKeyPair( Long id, KeyPair pair ) 293 { 294 lock.writeLock( ).lock( ); 295 try 296 { 297 this.keyPair.put( id, pair ); 298 } 299 finally 300 { 301 lock.writeLock( ).unlock( ); 302 } 303 } 304 305 private Long addNewKey( ) 306 { 307 final Long id = keyCounter.incrementAndGet( ); 308 if ( this.symmetricAlgorithm ) 309 { 310 addNewSecretKey( id, createNewSecretKey( this.signatureAlgorithm ) ); 311 } 312 else 313 { 314 addNewKeyPair( id, createNewKeyPair( this.signatureAlgorithm ) ); 315 } 316 return id; 317 } 318 319 private SecretKey getSecretKey( Long id ) 320 { 321 lock.readLock( ).lock( ); 322 try 323 { 324 return this.secretKey.get( id ); 325 } 326 finally 327 { 328 lock.readLock( ).unlock( ); 329 } 330 } 331 332 private KeyPair getKeyPair( Long id ) 333 { 334 lock.readLock( ).lock( ); 335 try 336 { 337 return this.keyPair.get( id ); 338 } 339 finally 340 { 341 lock.readLock( ).unlock( ); 342 } 343 } 344 345 private void handleKeyfile( ) 346 { 347 if ( !Files.exists( this.keystoreFilePath ) ) 348 { 349 final Long keyId = addNewKey( ); 350 if ( this.symmetricAlgorithm ) 351 { 352 try 353 { 354 writeSecretKey( this.keystoreFilePath, keyId, getSecretKey( keyId ) ); 355 } 356 catch ( IOException e ) 357 { 358 log.error( "Could not write Jwt key file {}: {}", this.keystoreFilePath, e.getMessage( ), e ); 359 log.warn( "Switching to in memory key handling " ); 360 this.fileStore = false; 361 } 362 } 363 else 364 { 365 try 366 { 367 writeKeyPair( this.keystoreFilePath, keyId, getKeyPair( keyId ) ); 368 } 369 catch ( IOException e ) 370 { 371 log.error( "Could not write Jwt key file {}: {}", this.keystoreFilePath, e.getMessage( ), e ); 372 log.warn( "Switching to in memory key handling " ); 373 this.fileStore = false; 374 } 375 } 376 } 377 else 378 { 379 if ( this.symmetricAlgorithm ) 380 { 381 try 382 { 383 final KeyHolder key = loadKeyFromFile( this.keystoreFilePath ); 384 keyCounter.set( key.getId() ); 385 addNewSecretKey( key.getId(), key.getSecretKey() ); 386 } 387 catch ( IOException e ) 388 { 389 log.error( "Could not read Jwt key file {}: {}", this.keystoreFilePath, e.getMessage( ), e ); 390 log.warn( "Switching to in memory key handling " ); 391 this.fileStore = false; 392 addNewKey( ); 393 } 394 } 395 else 396 { 397 try 398 { 399 final KeyHolder pair = loadPairFromFile( this.keystoreFilePath ); 400 keyCounter.set( pair.getId() ); 401 addNewKeyPair( pair.getId(), pair.getKeyPair() ); 402 } 403 catch ( Exception e ) 404 { 405 log.error( "Could not read Jwt key file {}: {}", this.keystoreFilePath, e.getMessage( ), e ); 406 log.warn( "Switching to in memory key handling " ); 407 this.fileStore = false; 408 addNewKey( ); 409 } 410 } 411 } 412 } 413 414 private SecretKey createNewSecretKey( String sigAlg ) 415 { 416 return Keys.secretKeyFor( SignatureAlgorithm.forName( sigAlg ) ); 417 } 418 419 private KeyPair createNewKeyPair( String sigAlg ) 420 { 421 return Keys.keyPairFor( SignatureAlgorithm.forName( sigAlg ) ); 422 } 423 424 private KeyHolder loadKeyFromFile( Path filePath ) throws IOException 425 { 426 if ( Files.exists( filePath ) ) 427 { 428 log.info( "Loading secret key from file storage {}", filePath ); 429 Properties props = new Properties( ); 430 try ( InputStream in = Files.newInputStream( filePath ) ) 431 { 432 props.loadFromXML( in ); 433 } 434 String algorithm = props.getProperty( PROP_PRIV_ALG ).trim( ); 435 String secretKey = props.getProperty( PROP_PRIVATEKEY ).trim( ); 436 Long keyId; 437 try { 438 keyId = Long.valueOf( props.getProperty( PROP_KEYID ) ); 439 } catch (NumberFormatException e) { 440 keyId = keyCounter.incrementAndGet( ); 441 } 442 byte[] keyData = Base64.getDecoder( ).decode( secretKey.getBytes( ) ); 443 return new KeyHolder( keyId, new SecretKeySpec( keyData, algorithm ) ); 444 } 445 else 446 { 447 throw new FileNotFoundException( "Keyfile does not exist " + filePath ); 448 } 449 } 450 451 452 private KeyHolder loadPairFromFile( Path filePath ) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException 453 { 454 if ( Files.exists( filePath ) ) 455 { 456 log.info( "Loading key pair from file storage {}", filePath ); 457 Properties props = new Properties( ); 458 try ( InputStream in = Files.newInputStream( filePath ) ) 459 { 460 props.loadFromXML( in ); 461 } 462 String algorithm = props.getProperty( PROP_PRIV_ALG ).trim( ); 463 String secretKeyBase64 = props.getProperty( PROP_PRIVATEKEY ).trim( ); 464 String publicKeyBase64 = props.getProperty( PROP_PUBLICKEY ).trim( ); 465 Long keyId; 466 try { 467 keyId = Long.valueOf( props.getProperty( PROP_KEYID ) ); 468 } catch (NumberFormatException e) { 469 keyId = keyCounter.incrementAndGet( ); 470 } 471 byte[] privateBytes = Base64.getDecoder( ).decode( secretKeyBase64 ); 472 byte[] publicBytes = Base64.getDecoder( ).decode( publicKeyBase64 ); 473 474 PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec( privateBytes ); 475 X509EncodedKeySpec publicSpec = new X509EncodedKeySpec( publicBytes ); 476 PrivateKey privateKey = KeyFactory.getInstance( algorithm ).generatePrivate( privateSpec ); 477 PublicKey publicKey = KeyFactory.getInstance( algorithm ).generatePublic( publicSpec ); 478 479 return new KeyHolder( keyId, new KeyPair( publicKey, privateKey ) ); 480 } 481 else 482 { 483 throw new FileNotFoundException( "Keyfile does not exist " + filePath ); 484 } 485 } 486 487 private void writeSecretKey( Path filePath, Long id, Key key ) throws IOException 488 { 489 log.info( "Writing secret key algorithm=" + key.getAlgorithm( ) + ", format=" + key.getFormat( ) + " to file " + filePath ); 490 Properties props = new Properties( ); 491 props.setProperty( PROP_PRIV_ALG, key.getAlgorithm( ) ); 492 if ( key.getFormat( ) != null ) 493 { 494 props.setProperty( PROP_PRIV_FORMAT, key.getFormat( ) ); 495 } 496 props.setProperty( PROP_KEYID, id.toString() ); 497 props.setProperty( PROP_PRIVATEKEY, Base64.getEncoder( ).encodeToString( key.getEncoded( ) ) ); 498 try ( OutputStream out = Files.newOutputStream( filePath ) ) 499 { 500 props.storeToXML( out, "Key for JWT signing" ); 501 } 502 try 503 { 504 Files.setPosixFilePermissions( filePath, PosixFilePermissions.fromString( "rw-------" ) ); 505 } 506 catch ( Exception e ) 507 { 508 log.error( "Could not set file permissions for {}: {}", filePath, e.getMessage( ), e ); 509 } 510 } 511 512 private void writeKeyPair( Path filePath, Long id, KeyPair keyPair ) throws IOException 513 { 514 PrivateKey privateKey = keyPair.getPrivate( ); 515 PublicKey publicKey = keyPair.getPublic( ); 516 517 log.info( "Writing private key algorithm=" + privateKey.getAlgorithm( ) + ", format=" + privateKey.getFormat( ) + " to file " + filePath ); 518 log.info( "Writing public key algorithm=" + publicKey.getAlgorithm( ) + ", format=" + publicKey.getFormat( ) + " to file " + filePath ); 519 Properties props = new Properties( ); 520 props.setProperty( PROP_PRIV_ALG, privateKey.getAlgorithm( ) ); 521 if ( privateKey.getFormat( ) != null ) 522 { 523 props.setProperty( PROP_PRIV_FORMAT, privateKey.getFormat( ) ); 524 } 525 props.setProperty( PROP_KEYID, id.toString( ) ); 526 props.setProperty( PROP_PUB_ALG, publicKey.getAlgorithm( ) ); 527 if ( publicKey.getFormat( ) != null ) 528 { 529 props.setProperty( PROP_PUB_FORMAT, publicKey.getFormat( ) ); 530 } 531 PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec( privateKey.getEncoded( ) ); 532 X509EncodedKeySpec publicSpec = new X509EncodedKeySpec( publicKey.getEncoded( ) ); 533 props.setProperty( PROP_PRIVATEKEY, Base64.getEncoder( ).encodeToString( privateSpec.getEncoded( ) ) ); 534 props.setProperty( PROP_PUBLICKEY, Base64.getEncoder( ).encodeToString( publicSpec.getEncoded( ) ) ); 535 536 try ( OutputStream out = Files.newOutputStream( filePath ) ) 537 { 538 props.storeToXML( out, "Key pair for JWT signing" ); 539 } 540 try 541 { 542 Files.setPosixFilePermissions( filePath, PosixFilePermissions.fromString( "rw-------" ) ); 543 } 544 catch ( Exception e ) 545 { 546 log.error( "Could not set file permissions for {}: {}", filePath, e.getMessage( ), e ); 547 } 548 549 } 550 551 /** 552 * Returns <code>true</code>, if the source is a instance of {@link TokenBasedAuthenticationDataSource} 553 * @param source the source to check 554 * @return <code>true</code>, if the given source is a instance of {@link TokenBasedAuthenticationDataSource} 555 */ 556 @Override 557 public boolean supportsDataSource( AuthenticationDataSource source ) 558 { 559 return ( source instanceof BearerTokenAuthenticationDataSource ); 560 } 561 562 /** 563 * Tries to verify the represented token and returns the result 564 * @param source the authentication source, which must be a {@link TokenBasedAuthenticationDataSource} 565 * @return the authentication result 566 * @throws AuthenticationException if the source is no {@link TokenBasedAuthenticationDataSource} 567 */ 568 public AuthenticationResult authenticate( BearerTokenAuthenticationDataSource source ) throws AuthenticationException 569 { 570 String jwt = source.getTokenData( ); 571 AuthenticationResult result; 572 try 573 { 574 String subject = verify( jwt ); 575 result = new AuthenticationResult( true, subject, null ); 576 } catch ( TokenAuthenticationException e) { 577 AuthenticationFailureCause cause = new AuthenticationFailureCause(e.getError().getId(), e.getMessage() ); 578 result = new AuthenticationResult( false, source.getUsername(), e, Arrays.asList( cause ) ); 579 } 580 return result; 581 } 582 583 @Override 584 public AuthenticationResult authenticate(AuthenticationDataSource dataSource) throws AuthenticationException 585 { 586 if (dataSource instanceof BearerTokenAuthenticationDataSource) { 587 return this.authenticate( (BearerTokenAuthenticationDataSource) dataSource ); 588 } 589 throw new AuthenticationException( "Authentication datasource not supported by this JwtAuthenticator" ); 590 } 591 592 /** 593 * Creates a new signing key and uses this for new tokens. It will keep {@link #maxInMemoryKeys} keys in the 594 * list for jwt verification. 595 */ 596 public Long renewSigningKey( ) 597 { 598 final Long id = addNewKey( ); 599 if (this.fileStore) 600 { 601 if ( this.symmetricAlgorithm ) 602 { 603 try 604 { 605 writeSecretKey( this.keystoreFilePath, id, getSecretKey( id ) ); 606 } 607 catch ( IOException e ) 608 { 609 log.error( "Could not write to keyfile {}: {}", this.keystoreFilePath, e.getMessage( ), e ); 610 } 611 } 612 else 613 { 614 try 615 { 616 writeKeyPair( this.keystoreFilePath, id, getKeyPair( id ) ); 617 } 618 catch ( IOException e ) 619 { 620 log.error( "Could not write to keyfile {}: {}", this.keystoreFilePath, e.getMessage( ), e ); 621 } 622 } 623 } 624 return id; 625 } 626 627 /** 628 * Simple internal DTO for keeping key data and its key id. 629 */ 630 private static class KeyHolder { 631 final Long id; 632 final SecretKey secretKey; 633 final KeyPair keyPair; 634 635 KeyHolder(Long id, SecretKey key) { 636 this.id = id; 637 this.secretKey = key; 638 this.keyPair = null; 639 } 640 KeyHolder(Long id, KeyPair key) { 641 this.id = id; 642 this.secretKey = null; 643 this.keyPair = key; 644 } 645 646 public Long getId( ) 647 { 648 return id; 649 } 650 651 public SecretKey getSecretKey( ) 652 { 653 return secretKey; 654 } 655 656 public KeyPair getKeyPair( ) 657 { 658 return keyPair; 659 } 660 661 public Key getSignerKey() { 662 return keyPair != null ? this.keyPair.getPrivate( ) : this.secretKey; 663 } 664 } 665 666 private KeyHolder getSignerKey() { 667 final Long id = keyCounter.get( ); 668 if (this.symmetricAlgorithm) { 669 return new KeyHolder( id, getSecretKey( id ) ); 670 } else { 671 return new KeyHolder( id, getKeyPair( id ) ); 672 } 673 } 674 675 /** 676 * Creates a token for the given user id. The token contains the following data: 677 * <ul> 678 * <li>the userid as subject</li> 679 * <li>a issuer archiva.apache.org/redback</li> 680 * <li>a id header with the key id</li> 681 * </ul>the user id as subject. 682 * 683 * @param userId the user identifier to set as subject 684 * @return the token string 685 */ 686 public Token generateToken( String userId ) 687 { 688 final KeyHolder signerKey = getSignerKey( ); 689 Instant now = Instant.now( ); 690 Instant expiration = now.plus( tokenLifetime ); 691 final String token = Jwts.builder( ) 692 .setSubject( userId ) 693 .setIssuer( ISSUER ) 694 .claim( TOKEN_TYPE, TokenType.ACCESS_TOKEN.getClaim( ) ) 695 .setIssuedAt( Date.from( now ) ) 696 .setExpiration( Date.from( expiration ) ) 697 .setHeaderParam( JwsHeader.KEY_ID, signerKey.getId( ).toString( ) ) 698 .signWith( signerKey.getSignerKey( ) ).compact( ); 699 TokenData metadata = new SimpleTokenData( userId, tokenLifetime, 0 ); 700 return new StringToken("", token, metadata ); 701 } 702 703 /** 704 * Creates a token for the given user id. The token contains the following data: 705 * <ul> 706 * <li>the userid as subject</li> 707 * <li>a issuer archiva.apache.org/redback</li> 708 * <li>a id header with the key id</li> 709 * </ul>the user id as subject. 710 * 711 * @param userId the user identifier to set as subject 712 * @param type the token type that indicates if this token is a access or refresh token 713 * @return the token string 714 */ 715 public Token generateToken( String userId, TokenType type ) 716 { 717 if (type==TokenType.ACCESS_TOKEN) { 718 return generateToken( userId ); 719 } else if (type == TokenType.REFRESH_TOKEN) 720 { 721 return generateRefreshToken( userId ); 722 } else { 723 throw new RuntimeException( "Invalid token type requested" ); 724 } 725 } 726 727 private Token generateRefreshToken(String userId) { 728 final KeyHolder signerKey = getSignerKey( ); 729 Instant now = Instant.now( ); 730 Instant expiration = now.plus( refreshTokenLifetime ); 731 final String id = UUID.randomUUID( ).toString( ); 732 final String token = Jwts.builder( ) 733 .setSubject( userId ) 734 .setIssuer( ISSUER ) 735 .setIssuedAt( Date.from( now ) ) 736 .setId( id ) 737 .claim( TOKEN_TYPE, TokenType.REFRESH_TOKEN.getClaim() ) 738 .setExpiration( Date.from( expiration ) ) 739 .setHeaderParam( JwsHeader.KEY_ID, signerKey.getId( ).toString( ) ) 740 .signWith( signerKey.getSignerKey( ) ).compact( ); 741 TokenData metadata = new SimpleTokenData( userId, refreshTokenLifetime, 0 ); 742 return new StringToken( TokenType.REFRESH_TOKEN, id, token, metadata ); 743 } 744 745 /** 746 * Returns a token object from the given token String 747 * 748 * @param tokenData the string representation of the token 749 * @return the token instance 750 */ 751 public Token tokenFromString(String tokenData) { 752 Jws<Claims> parsedToken = parseToken( tokenData ); 753 String userId = parsedToken.getBody( ).getSubject( ); 754 TokenType type = TokenType.ofClaim( parsedToken.getBody( ).get( TOKEN_TYPE, String.class ) ); 755 String id = parsedToken.getBody( ).getId( ); 756 Instant expiration = parsedToken.getBody( ).getExpiration( ).toInstant( ); 757 Instant issuedAt = parsedToken.getBody( ).getIssuedAt( ).toInstant( ); 758 long lifetime = Duration.between( issuedAt, expiration ).toMillis( ); 759 TokenData metadata = new SimpleTokenData( userId, lifetime, 0 ); 760 return new StringToken( type, id, tokenData, metadata ); 761 } 762 763 /** 764 * Allows to renew a token based on the origin token. If the presented <code>origin</code> 765 * is valid, a new token with refreshed expiration time will be returned. 766 * 767 * @param refreshToken the refresh token 768 * @return the newly created token 769 * @throws AuthenticationException if the given origin token is not valid 770 */ 771 public Token refreshAccessToken( String refreshToken) throws TokenAuthenticationException { 772 try 773 { 774 String subject = verify( refreshToken, TokenType.REFRESH_TOKEN ); 775 return generateToken( subject ); 776 } catch ( JwtException e) { 777 throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "unknown error " + e.getMessage( ) ); 778 } 779 } 780 781 /** 782 * Parses the given token and returns the JWS metadata stored in the token. 783 * 784 * @param token the token string 785 * @return the parsed data 786 * @throws JwtException if the token data is not valid anymore 787 */ 788 public Jws<Claims> parseToken( String token) throws JwtException { 789 return getParser(TokenType.ALL).parseClaimsJws( token ); 790 } 791 792 /** 793 * Verifies the given JWT Token and returns the stored subject, if successful 794 * If the verification failed a TokenAuthenticationException is thrown. 795 * @param token the JWT representation 796 * @return the subject of the JWT 797 * @throws TokenAuthenticationException if the verification failed 798 */ 799 public String verify( String token ) throws TokenAuthenticationException 800 { 801 return verify( token, TokenType.ACCESS_TOKEN ); 802 } 803 804 public String verify( String token, TokenType type ) throws TokenAuthenticationException 805 { 806 try 807 { 808 Jws<Claims> signature = getParser(type).parseClaimsJws( token ); 809 String subject = signature.getBody( ).getSubject( ); 810 if ( StringUtils.isEmpty( subject ) ) 811 { 812 throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "contains no subject" ); 813 } 814 return subject; 815 } 816 catch ( ExpiredJwtException e ) 817 { 818 throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "token expired" ); 819 } 820 catch ( SignatureException e ) { 821 throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "token signature does not match" ); 822 } 823 catch ( UnsupportedJwtException e) { 824 throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "jwt is unsupported" ); 825 } 826 catch ( MalformedJwtException e) 827 { 828 throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "malformed token content" ); 829 } 830 catch (JwtKeyIdNotFoundException e) { 831 throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "signer key does not exist" ); 832 } 833 catch ( MissingClaimException |IncorrectClaimException e ) { 834 throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "the token type is not correct - expected claim "+type.getClaim() ); 835 } 836 catch ( JwtException e) { 837 log.debug( "Unknown JwtException {}, {}", e.getClass( ), e.getMessage( ) ); 838 throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "unknown error " + e.getMessage( ) ); 839 } 840 841 } 842 843 /** 844 * Removes all signing keys and creates a new one. If you call this method, all JWT tokens generated before, 845 * will be invalid. 846 */ 847 public void revokeSigningKeys() { 848 lock.writeLock( ).lock( ); 849 try { 850 this.secretKey.clear(); 851 this.keyPair.clear(); 852 renewSigningKey( ); 853 } finally 854 { 855 lock.writeLock( ).unlock( ); 856 } 857 } 858 859 private SigningKeyResolver getResolver( ) 860 { 861 return this.resolver; 862 } 863 864 /** 865 * Returns <code>true</code>, if the signature algorithm ist a symmetric one, otherwise <code>false</code> 866 * @return <code>true</code>, if symmetric algorithm, otherwise <code>false</code> 867 */ 868 public boolean usesSymmetricAlgorithm( ) 869 { 870 return symmetricAlgorithm; 871 } 872 873 /** 874 * Returns the signature algorithm used for signing JWT tokens 875 * @return the string representation of the signature algorithm 876 */ 877 public String getSignatureAlgorithm( ) 878 { 879 return signatureAlgorithm; 880 } 881 882 /** 883 * Returns the keystore type that is setup for the authenticator 884 * @return either <code>memory</code> or <code>plainfile</code> 885 */ 886 public String getKeystoreType( ) 887 { 888 return keystoreType; 889 } 890 891 /** 892 * Returns the path to the keystore file or <code>null</code>, if the keystore type is <code>memory</code> 893 * @return the path to the keystore file, or <code>null</code> 894 */ 895 public Path getKeystoreFilePath( ) 896 { 897 return keystoreFilePath; 898 } 899 900 /** 901 * Returns the maximum number of signature keys to store in memory for verification 902 * @return the maximum number of signature keys to keep in memory 903 */ 904 public int getMaxInMemoryKeys( ) 905 { 906 return maxInMemoryKeys; 907 } 908 909 /** 910 * Returns the current size of the in memory key list 911 * @return the number of memory stored signature keys 912 */ 913 public int getCurrentKeyListSize() { 914 if (symmetricAlgorithm) { 915 return secretKey.size( ); 916 } else { 917 return keyPair.size( ); 918 } 919 } 920 921 /** 922 * Returns the current used key identifier. 923 * @return the key identifier 924 */ 925 public Long getCurrentKeyId() { 926 return keyCounter.get( ); 927 } 928 929 /** 930 * Returns the default token lifetime of generated tokens. 931 * @return the lifetime as duration 932 */ 933 public Duration getTokenLifetime() { 934 return this.tokenLifetime; 935 } 936 937 /** 938 * Sets the default token lifetime of generated tokens. 939 * @param lifetime the lifetime as duration 940 */ 941 public void setTokenLifetime(Duration lifetime) { 942 this.tokenLifetime = lifetime; 943 } 944 945 public UserConfiguration getUserConfiguration( ) 946 { 947 return userConfiguration; 948 } 949 950 public void setUserConfiguration( UserConfiguration userConfiguration ) 951 { 952 this.userConfiguration = userConfiguration; 953 } 954}