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 java.nio.charset.StandardCharsets; 022import java.security.MessageDigest; 023import java.security.NoSuchAlgorithmException; 024import java.util.Date; 025import java.util.Map; 026import java.util.Timer; 027import java.util.TimerTask; 028import java.util.concurrent.ConcurrentHashMap; 029 030import net.jcip.annotations.ThreadSafe; 031 032import com.nimbusds.jose.util.Base64URL; 033import com.nimbusds.oauth2.sdk.id.JWTID; 034import com.nimbusds.oauth2.sdk.util.singleuse.AlreadyUsedException; 035import com.nimbusds.oauth2.sdk.util.singleuse.SingleUseChecker; 036 037 038/** 039 * DPoP proof JWT single use checker. Caches a hash of the checked DPoP JWT 040 * "jti" (JWT ID) claims for a given DPoP issuer. The checker should be 041 * {@link #shutdown() shut down} when no longer in use. 042 */ 043@ThreadSafe 044public class DefaultDPoPSingleUseChecker implements SingleUseChecker<Map.Entry<DPoPIssuer, JWTID>> { 045 046 private final Timer timer; 047 048 private final ConcurrentHashMap<String,Long> cachedJTIs = new ConcurrentHashMap<>(); 049 050 051 /** 052 * Creates a new DPoP proof JWT single use checker. 053 * 054 * @param lifetimeSeconds The lifetime of cached DPoP proof "jti" 055 * (JWT ID) claims, in seconds. 056 * @param purgeIntervalSeconds The interval in seconds for purging the 057 * cached "jti" (JWT ID) claims of checked 058 * DPoP proofs. 059 */ 060 public DefaultDPoPSingleUseChecker(final long lifetimeSeconds, 061 final long purgeIntervalSeconds) { 062 063 timer = new Timer("dpop-single-use-jti-cache-purge-task", true); 064 065 timer.schedule( 066 new TimerTask() { 067 @Override 068 public void run() { 069 final long nowMS = new Date().getTime(); 070 final long expHorizon = nowMS - lifetimeSeconds * 1000; 071 for (Map.Entry<String, Long> en: cachedJTIs.entrySet()) { 072 if (en.getValue() < expHorizon) { 073 cachedJTIs.remove(en.getKey()); 074 } 075 } 076 } 077 }, 078 purgeIntervalSeconds * 1000, 079 purgeIntervalSeconds * 1000); 080 } 081 082 083 /** 084 * Computes a SHA-256 hash for the specified access token. 085 * 086 * @param jti The access token. Must not be {@code null}. 087 * 088 * @return The hash, BASE64 URL encoded. 089 * 090 * @throws RuntimeException If hashing failed. 091 */ 092 private static Base64URL computeSHA256(final JWTID jti) { 093 094 byte[] hash; 095 try { 096 MessageDigest md = MessageDigest.getInstance("SHA-256"); 097 hash = md.digest(jti.getValue().getBytes(StandardCharsets.UTF_8)); 098 } catch (NoSuchAlgorithmException e) { 099 throw new RuntimeException(e.getMessage(), e); 100 } 101 102 return Base64URL.encode(hash); 103 } 104 105 106 @Override 107 public void markAsUsed(final Map.Entry<DPoPIssuer, JWTID> object) 108 throws AlreadyUsedException { 109 110 String key = object.getKey().getValue() + ":" + computeSHA256(object.getValue()); 111 112 long nowMS = new Date().getTime(); 113 114 if (cachedJTIs.putIfAbsent(key, nowMS) != null) { 115 throw new AlreadyUsedException("Detected jti replay"); 116 } 117 } 118 119 120 /** 121 * Returns the number of cached items. 122 * 123 * @return The cached items, zero if none. 124 */ 125 public int getCacheSize() { 126 127 return cachedJTIs.size(); 128 } 129 130 131 /** 132 * Shuts down this checker and frees any associated resources. 133 */ 134 public void shutdown() { 135 136 timer.cancel(); 137 } 138}