From dfb270a6e171b0808f92701128e41261c53faf3f Mon Sep 17 00:00:00 2001 From: Matt Ward Date: Mon, 9 Oct 2017 14:39:21 +0100 Subject: [PATCH] REPO-2575: allow sending 'AlfTicket' scheme in WWW-Authenticate header "401 response with www-authenticate header causes browser native login prompt to be shown." By sending: WWW-Authenticate: AlfTicket realm="..." We can avoid making the browser pop up a Basic auth dialogue box. This is particularly useful for apps built for the browser that talk directly to the Alfresco public APIs at the backend. To use this feature, set alfresco.restApi.basicAuthScheme=false --- .../api/PublicApiAuthenticatorFactory.java | 72 ++++++++++++------- .../resources/alfresco/project.properties | 24 +++++++ .../alfresco/public-rest-context.xml | 1 + .../rest/api/tests/AuthenticationsTest.java | 38 ++++++++++ 4 files changed, 109 insertions(+), 26 deletions(-) create mode 100644 src/main/resources/alfresco/project.properties diff --git a/src/main/java/org/alfresco/rest/api/PublicApiAuthenticatorFactory.java b/src/main/java/org/alfresco/rest/api/PublicApiAuthenticatorFactory.java index 0b18efa24a..2eef57809f 100644 --- a/src/main/java/org/alfresco/rest/api/PublicApiAuthenticatorFactory.java +++ b/src/main/java/org/alfresco/rest/api/PublicApiAuthenticatorFactory.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.rest.api; import java.util.Collections; @@ -67,6 +67,7 @@ public class PublicApiAuthenticatorFactory extends RemoteUserAuthenticatorFactor private TenantAuthentication tenantAuthentication; private Set validAuthenticatorKeys = Collections.emptySet(); private Set outboundHeaderNames; + private boolean useBasicAuth = true; public void setAuthenticatorKeyHeader(String authenticatorKeyHeader) { @@ -92,6 +93,23 @@ public void setOutboundHeaders(Set outboundHeaders) this.outboundHeaderNames = outboundHeaders; } + /** + * Whether to suggest that users use Basic auth. If set to true, then a + * 401 (unauthorized) response will contain a WWW-Authentication header + * specifying the scheme "Basic". If this is set to false, then + * the scheme "AlfTicket" will be used. + *

+ * Set this to false to avoid getting Basic auth dialogue popups in browsers + * when using the public API directly, for example. + * + * @see REPO-2575 + * @param useBasicAuth + */ + public void setUseBasicAuth(boolean useBasicAuth) + { + this.useBasicAuth = useBasicAuth; + } + public void setTenantAuthentication(TenantAuthentication service) { this.tenantAuthentication = service; @@ -232,7 +250,9 @@ public Boolean execute() throws Exception if (!authorized) { servletRes.setStatus(401); - servletRes.setHeader("WWW-Authenticate", "Basic realm=\"Alfresco " + servletReq.getTenant() + " tenant\""); + String scheme = useBasicAuth ? "Basic" : "AlfTicket"; + String challenge = scheme + " realm=\"Alfresco " + servletReq.getTenant() + " tenant\""; + servletRes.setHeader("WWW-Authenticate", challenge); } } } diff --git a/src/main/resources/alfresco/project.properties b/src/main/resources/alfresco/project.properties new file mode 100644 index 0000000000..90aa30f32e --- /dev/null +++ b/src/main/resources/alfresco/project.properties @@ -0,0 +1,24 @@ +################################################################################ +# Remote API property defaults +# 9th October 2017 +################################################################################ + + +# Whether to send a "basic auth" challenge along with a 401 response (not authorized) +# +# If set to true, then a header will be sent similar to: +# +# WWW-Authenticate: Basic realm="..." +# +# If set to false, then a header will be sent with an AlfTicket challenge: +# +# WWW-Authenticate: AlfTicket realm="..." +# +# This latter case is particularly useful when building a web-browser based client +# that communicates directly with the Alfresco Public API - using the AlfTicket +# challenge allows the client to completely control the login behaviour, whereas +# allowing a Basic auth challenge to be sent results in the Basic Authentication +# browser dialogue being popped-up without the client app being involved. +# +# See issue REPO-2575 for details. +alfresco.restApi.basicAuthScheme=true diff --git a/src/main/resources/alfresco/public-rest-context.xml b/src/main/resources/alfresco/public-rest-context.xml index 997a45b348..641894a2a1 100644 --- a/src/main/resources/alfresco/public-rest-context.xml +++ b/src/main/resources/alfresco/public-rest-context.xml @@ -95,6 +95,7 @@ + diff --git a/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java b/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java index 61ce4829d0..5b50d7ea1f 100644 --- a/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java +++ b/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java @@ -31,6 +31,7 @@ import org.alfresco.rest.AbstractSingleNetworkSiteTest; import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.People; +import org.alfresco.rest.api.PublicApiAuthenticatorFactory; import org.alfresco.rest.api.model.LoginTicket; import org.alfresco.rest.api.model.LoginTicketResponse; import org.alfresco.rest.api.sites.SiteEntityResource; @@ -41,6 +42,7 @@ import org.alfresco.rest.api.tests.client.data.Folder; import org.alfresco.rest.api.tests.util.RestApiUtil; import org.apache.commons.codec.binary.Base64; +import org.junit.Before; import org.junit.Test; import java.util.Collections; @@ -56,7 +58,43 @@ public class AuthenticationsTest extends AbstractSingleNetworkSiteTest { private static final String TICKETS_URL = "tickets"; private static final String TICKETS_API_NAME = "authentication"; + private PublicApiAuthenticatorFactory authFactory; + @Before + public void setUpAuthTest() + { + authFactory = (PublicApiAuthenticatorFactory) applicationContext.getBean("publicapi.authenticator"); + } + + @Test + public void canDisableBasicAuthChallenge() throws Exception + { + authFactory.setUseBasicAuth(false); + + // Expect to be challenged for an AlfTicket (REPO-2575) + testAuthChallenge("AlfTicket"); + } + + @Test + public void canEnableBasicAuthChallenge() throws Exception + { + authFactory.setUseBasicAuth(true); + + // Expect to be challenged for Basic auth. + testAuthChallenge("Basic"); + } + + private void testAuthChallenge(String expectedScheme) throws Exception + { + // Unauthorized call + setRequestContext(null); + + HttpResponse response = getAll(SiteEntityResource.class, getPaging(0, 100), null, 401); + String authenticateHeader = response.getHeaders().get("WWW-Authenticate"); + assertNotNull("Expected an authentication challenge", authenticateHeader); + String authScheme = authenticateHeader.split(" ")[0]; // Other parts may contain, e.g. realm="..." + assertEquals(expectedScheme, authScheme); + } /** * Tests login (create ticket), logout (delete ticket), and validate (get ticket).