001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2021, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk.dpop.verifiers;
019
020
021import com.nimbusds.jose.util.Base64URL;
022import com.nimbusds.oauth2.sdk.id.JWTID;
023import com.nimbusds.oauth2.sdk.util.singleuse.AlreadyUsedException;
024import com.nimbusds.oauth2.sdk.util.singleuse.SingleUseChecker;
025import net.jcip.annotations.ThreadSafe;
026
027import java.nio.charset.StandardCharsets;
028import java.security.MessageDigest;
029import java.security.NoSuchAlgorithmException;
030import java.util.Date;
031import java.util.Map;
032import java.util.Timer;
033import java.util.TimerTask;
034import java.util.concurrent.ConcurrentHashMap;
035
036
037/**
038 * In-memory DPoP proof JWT single use checker. Caches a hash of the checked
039 * DPoP JWT "jti" (JWT ID) claims for a given DPoP issuer. {@link #shutdown()
040 * Shut down} the checker when no longer in use.
041 */
042@ThreadSafe
043public class InMemoryDPoPSingleUseChecker implements SingleUseChecker<DPoPProofUse> {
044
045        private final Timer timer;
046
047        private final ConcurrentHashMap<String,Long> cachedJTIs = new ConcurrentHashMap<>();
048
049
050        /**
051         * Creates a new DPoP proof JWT single use checker.
052         *
053         * @param lifetimeSeconds      The lifetime of cached DPoP proof "jti"
054         *                             (JWT ID) claims, in seconds.
055         * @param purgeIntervalSeconds The interval in seconds for purging the
056         *                             cached "jti" (JWT ID) claims of checked
057         *                             DPoP proofs.
058         */
059        public InMemoryDPoPSingleUseChecker(final long lifetimeSeconds,
060                                            final long purgeIntervalSeconds) {
061                
062                timer = new Timer("dpop-single-use-jti-cache-purge-task", true);
063                
064                timer.schedule(
065                        new TimerTask() {
066                                @Override
067                                public void run() {
068                                        final long nowMS = new Date().getTime();
069                                        final long expHorizon = nowMS - lifetimeSeconds * 1000;
070                                        for (Map.Entry<String, Long> en: cachedJTIs.entrySet()) {
071                                                if (en.getValue() < expHorizon) {
072                                                        cachedJTIs.remove(en.getKey());
073                                                }
074                                        }
075                                }
076                        },
077                        purgeIntervalSeconds * 1000,
078                        purgeIntervalSeconds * 1000);
079        }
080        
081        
082        /**
083         * Computes the SHA-256 hash for the specified access token.
084         *
085         * @param jti The access token. Must not be {@code null}.
086         *
087         * @return The hash, BASE64 URL encoded.
088         *
089         * @throws RuntimeException If hashing failed.
090         */
091        static Base64URL computeSHA256(final JWTID jti) {
092                
093                byte[] hash;
094                try {
095                        MessageDigest md = MessageDigest.getInstance("SHA-256");
096                        hash = md.digest(jti.getValue().getBytes(StandardCharsets.UTF_8));
097                } catch (NoSuchAlgorithmException e) {
098                        throw new RuntimeException(e.getMessage(), e);
099                }
100                
101                return Base64URL.encode(hash);
102        }
103        
104        
105        @Override
106        public void markAsUsed(final DPoPProofUse dPoPProofUse)
107                throws AlreadyUsedException {
108                
109                String key = dPoPProofUse.getIssuer()+ ":" + computeSHA256(dPoPProofUse.getJWTID());
110                
111                long nowMS = new Date().getTime();
112                
113                if (cachedJTIs.putIfAbsent(key, nowMS) != null) {
114                        throw new AlreadyUsedException("Detected jti replay");
115                }
116        }
117        
118        
119        /**
120         * Returns the number of cached items.
121         *
122         * @return The cached items, zero if none.
123         */
124        public int getCacheSize() {
125                
126                return cachedJTIs.size();
127        }
128        
129        
130        /**
131         * Shuts down this checker and frees any associated resources.
132         */
133        public void shutdown() {
134                
135                timer.cancel();
136        }
137}