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}