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}