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}