001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2015 Richard "Shred" Körber 005 * http://acme4j.shredzone.org 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 013 */ 014package org.shredzone.acme4j; 015 016import static org.assertj.core.api.Assertions.assertThat; 017import static org.assertj.core.api.Assertions.within; 018import static org.junit.jupiter.api.Assertions.assertThrows; 019import static org.shredzone.acme4j.toolbox.TestUtils.getJSON; 020import static org.shredzone.acme4j.toolbox.TestUtils.url; 021 022import java.io.IOException; 023import java.net.HttpURLConnection; 024import java.net.URL; 025import java.time.Duration; 026import java.time.Instant; 027import java.time.temporal.ChronoUnit; 028import java.util.Optional; 029import java.util.concurrent.atomic.AtomicBoolean; 030 031import org.junit.jupiter.api.Test; 032import org.shredzone.acme4j.challenge.Challenge; 033import org.shredzone.acme4j.challenge.Dns01Challenge; 034import org.shredzone.acme4j.challenge.Http01Challenge; 035import org.shredzone.acme4j.challenge.TlsAlpn01Challenge; 036import org.shredzone.acme4j.exception.AcmeProtocolException; 037import org.shredzone.acme4j.provider.TestableConnectionProvider; 038import org.shredzone.acme4j.toolbox.JSON; 039import org.shredzone.acme4j.toolbox.JSONBuilder; 040 041/** 042 * Unit tests for {@link Authorization}. 043 */ 044public class AuthorizationTest { 045 046 private static final String SNAILMAIL_TYPE = "snail-01"; // a non-existent challenge 047 private static final String DUPLICATE_TYPE = "duplicate-01"; // a duplicate challenge 048 049 private final URL locationUrl = url("http://example.com/acme/account"); 050 051 /** 052 * Test that {@link Authorization#findChallenge(String)} finds challenges. 053 */ 054 @Test 055 public void testFindChallenge() throws IOException { 056 var authorization = createChallengeAuthorization(); 057 058 // A snail mail challenge is not available at all 059 var c1 = authorization.findChallenge(SNAILMAIL_TYPE); 060 assertThat(c1).isEmpty(); 061 062 // HttpChallenge is available 063 var c2 = authorization.findChallenge(Http01Challenge.TYPE); 064 assertThat(c2).isNotEmpty(); 065 assertThat(c2.get()).isInstanceOf(Http01Challenge.class); 066 067 // Dns01Challenge is available 068 var c3 = authorization.findChallenge(Dns01Challenge.TYPE); 069 assertThat(c3).isNotEmpty(); 070 assertThat(c3.get()).isInstanceOf(Dns01Challenge.class); 071 072 // TlsAlpn01Challenge is available 073 var c4 = authorization.findChallenge(TlsAlpn01Challenge.TYPE); 074 assertThat(c4).isNotEmpty(); 075 assertThat(c4.get()).isInstanceOf(TlsAlpn01Challenge.class); 076 } 077 078 /** 079 * Test that {@link Authorization#findChallenge(Class)} finds challenges. 080 */ 081 @Test 082 public void testFindChallengeByType() throws IOException { 083 var authorization = createChallengeAuthorization(); 084 085 // A snail mail challenge is not available at all 086 var c1 = authorization.findChallenge(NonExistingChallenge.class); 087 assertThat(c1).isEmpty(); 088 089 // HttpChallenge is available 090 var c2 = authorization.findChallenge(Http01Challenge.class); 091 assertThat(c2).isNotEmpty(); 092 093 // Dns01Challenge is available 094 var c3 = authorization.findChallenge(Dns01Challenge.class); 095 assertThat(c3).isNotEmpty(); 096 097 // TlsAlpn01Challenge is available 098 var c4 = authorization.findChallenge(TlsAlpn01Challenge.class); 099 assertThat(c4).isNotEmpty(); 100 } 101 102 /** 103 * Test that {@link Authorization#findChallenge(String)} fails on duplicate 104 * challenges. 105 */ 106 @Test 107 public void testFailDuplicateChallenges() { 108 assertThrows(AcmeProtocolException.class, () -> { 109 var authorization = createChallengeAuthorization(); 110 authorization.findChallenge(DUPLICATE_TYPE); 111 }); 112 } 113 114 /** 115 * Test that authorization is properly updated. 116 */ 117 @Test 118 public void testUpdate() throws Exception { 119 var provider = new TestableConnectionProvider() { 120 @Override 121 public int sendSignedPostAsGetRequest(URL url, Login login) { 122 assertThat(url).isEqualTo(locationUrl); 123 return HttpURLConnection.HTTP_OK; 124 } 125 126 @Override 127 public JSON readJsonResponse() { 128 return getJSON("updateAuthorizationResponse"); 129 } 130 }; 131 132 var login = provider.createLogin(); 133 134 provider.putTestChallenge("http-01", Http01Challenge::new); 135 provider.putTestChallenge("dns-01", Dns01Challenge::new); 136 provider.putTestChallenge("tls-alpn-01", TlsAlpn01Challenge::new); 137 138 var auth = new Authorization(login, locationUrl); 139 auth.update(); 140 141 assertThat(auth.getIdentifier().getDomain()).isEqualTo("example.org"); 142 assertThat(auth.getStatus()).isEqualTo(Status.VALID); 143 assertThat(auth.isWildcard()).isFalse(); 144 assertThat(auth.getExpires().orElseThrow()).isCloseTo("2016-01-02T17:12:40Z", within(1, ChronoUnit.SECONDS)); 145 assertThat(auth.getLocation()).isEqualTo(locationUrl); 146 147 assertThat(auth.getChallenges()).containsExactlyInAnyOrder( 148 provider.getChallenge(Http01Challenge.TYPE), 149 provider.getChallenge(Dns01Challenge.TYPE), 150 provider.getChallenge(TlsAlpn01Challenge.TYPE)); 151 152 provider.close(); 153 } 154 155 /** 156 * Test that wildcard authorization are correct. 157 */ 158 @Test 159 public void testWildcard() throws Exception { 160 var provider = new TestableConnectionProvider() { 161 @Override 162 public int sendSignedPostAsGetRequest(URL url, Login login) { 163 assertThat(url).isEqualTo(locationUrl); 164 return HttpURLConnection.HTTP_OK; 165 } 166 167 @Override 168 public JSON readJsonResponse() { 169 return getJSON("updateAuthorizationWildcardResponse"); 170 } 171 }; 172 173 var login = provider.createLogin(); 174 175 provider.putTestChallenge("dns-01", Dns01Challenge::new); 176 177 var auth = new Authorization(login, locationUrl); 178 auth.update(); 179 180 assertThat(auth.getIdentifier().getDomain()).isEqualTo("example.org"); 181 assertThat(auth.getStatus()).isEqualTo(Status.VALID); 182 assertThat(auth.isWildcard()).isTrue(); 183 assertThat(auth.getExpires().orElseThrow()).isCloseTo("2016-01-02T17:12:40Z", within(1, ChronoUnit.SECONDS)); 184 assertThat(auth.getLocation()).isEqualTo(locationUrl); 185 186 assertThat(auth.getChallenges()).containsExactlyInAnyOrder( 187 provider.getChallenge(Dns01Challenge.TYPE)); 188 189 provider.close(); 190 } 191 192 /** 193 * Test lazy loading. 194 */ 195 @Test 196 public void testLazyLoading() throws Exception { 197 var requestWasSent = new AtomicBoolean(false); 198 199 var provider = new TestableConnectionProvider() { 200 @Override 201 public int sendSignedPostAsGetRequest(URL url, Login login) { 202 requestWasSent.set(true); 203 assertThat(url).isEqualTo(locationUrl); 204 return HttpURLConnection.HTTP_OK; 205 } 206 207 @Override 208 public JSON readJsonResponse() { 209 return getJSON("updateAuthorizationResponse"); 210 } 211 }; 212 213 var login = provider.createLogin(); 214 215 provider.putTestChallenge("http-01", Http01Challenge::new); 216 provider.putTestChallenge("dns-01", Dns01Challenge::new); 217 provider.putTestChallenge("tls-alpn-01", TlsAlpn01Challenge::new); 218 219 var auth = new Authorization(login, locationUrl); 220 221 // Lazy loading 222 assertThat(requestWasSent).isFalse(); 223 assertThat(auth.getIdentifier().getDomain()).isEqualTo("example.org"); 224 assertThat(requestWasSent).isTrue(); 225 226 // Subsequent queries do not trigger another load 227 requestWasSent.set(false); 228 assertThat(auth.getIdentifier().getDomain()).isEqualTo("example.org"); 229 assertThat(auth.getStatus()).isEqualTo(Status.VALID); 230 assertThat(auth.isWildcard()).isFalse(); 231 assertThat(auth.getExpires().orElseThrow()).isCloseTo("2016-01-02T17:12:40Z", within(1, ChronoUnit.SECONDS)); 232 assertThat(requestWasSent).isFalse(); 233 234 provider.close(); 235 } 236 237 /** 238 * Test that authorization is properly updated, with retry-after header set. 239 */ 240 @Test 241 public void testUpdateRetryAfter() throws Exception { 242 var retryAfter = Instant.now().plus(Duration.ofSeconds(30)); 243 244 var provider = new TestableConnectionProvider() { 245 @Override 246 public int sendSignedPostAsGetRequest(URL url, Login login) { 247 assertThat(url).isEqualTo(locationUrl); 248 return HttpURLConnection.HTTP_OK; 249 } 250 251 @Override 252 public JSON readJsonResponse() { 253 return getJSON("updateAuthorizationResponse"); 254 } 255 256 @Override 257 public Optional<Instant> getRetryAfter() { 258 return Optional.of(retryAfter); 259 } 260 }; 261 262 var login = provider.createLogin(); 263 264 provider.putTestChallenge("http-01", Http01Challenge::new); 265 provider.putTestChallenge("dns-01", Dns01Challenge::new); 266 provider.putTestChallenge("tls-alpn-01", TlsAlpn01Challenge::new); 267 268 var auth = new Authorization(login, locationUrl); 269 var returnedRetryAfter = auth.fetch(); 270 assertThat(returnedRetryAfter).hasValue(retryAfter); 271 272 assertThat(auth.getIdentifier().getDomain()).isEqualTo("example.org"); 273 assertThat(auth.getStatus()).isEqualTo(Status.VALID); 274 assertThat(auth.isWildcard()).isFalse(); 275 assertThat(auth.getExpires().orElseThrow()).isCloseTo("2016-01-02T17:12:40Z", within(1, ChronoUnit.SECONDS)); 276 assertThat(auth.getLocation()).isEqualTo(locationUrl); 277 278 assertThat(auth.getChallenges()).containsExactlyInAnyOrder( 279 provider.getChallenge(Http01Challenge.TYPE), 280 provider.getChallenge(Dns01Challenge.TYPE), 281 provider.getChallenge(TlsAlpn01Challenge.TYPE)); 282 283 provider.close(); 284 } 285 286 /** 287 * Test that an authorization can be deactivated. 288 */ 289 @Test 290 public void testDeactivate() throws Exception { 291 var provider = new TestableConnectionProvider() { 292 @Override 293 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 294 var json = claims.toJSON(); 295 assertThat(json.get("status").asString()).isEqualTo("deactivated"); 296 assertThat(url).isEqualTo(locationUrl); 297 assertThat(login).isNotNull(); 298 return HttpURLConnection.HTTP_OK; 299 } 300 301 @Override 302 public JSON readJsonResponse() { 303 return getJSON("updateAuthorizationResponse"); 304 } 305 }; 306 307 var login = provider.createLogin(); 308 309 provider.putTestChallenge("http-01", Http01Challenge::new); 310 provider.putTestChallenge("dns-01", Dns01Challenge::new); 311 provider.putTestChallenge("tls-alpn-01", TlsAlpn01Challenge::new); 312 313 var auth = new Authorization(login, locationUrl); 314 auth.deactivate(); 315 316 provider.close(); 317 } 318 319 /** 320 * Creates an {@link Authorization} instance with a set of challenges. 321 */ 322 private Authorization createChallengeAuthorization() throws IOException { 323 try (var provider = new TestableConnectionProvider()) { 324 var login = provider.createLogin(); 325 326 provider.putTestChallenge(Http01Challenge.TYPE, Http01Challenge::new); 327 provider.putTestChallenge(Dns01Challenge.TYPE, Dns01Challenge::new); 328 provider.putTestChallenge(TlsAlpn01Challenge.TYPE, TlsAlpn01Challenge::new); 329 provider.putTestChallenge(DUPLICATE_TYPE, Challenge::new); 330 331 var authorization = new Authorization(login, locationUrl); 332 authorization.setJSON(getJSON("authorizationChallenges")); 333 return authorization; 334 } 335 } 336 337 /** 338 * Dummy challenge that is never going to be created. 339 */ 340 private static class NonExistingChallenge extends Challenge { 341 public NonExistingChallenge(Login login, JSON data) { 342 super(login, data); 343 } 344 } 345 346}