Permalink
Browse files

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
  • Loading branch information...
mattward committed Oct 9, 2017
1 parent 1742e74 commit dfb270a6e171b0808f92701128e41261c53faf3f
@@ -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 <http://www.gnu.org/licenses/>.
* #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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api;
import java.util.Collections;
@@ -67,6 +67,7 @@
private TenantAuthentication tenantAuthentication;
private Set<String> validAuthenticatorKeys = Collections.emptySet();
private Set<String> outboundHeaderNames;
private boolean useBasicAuth = true;
public void setAuthenticatorKeyHeader(String authenticatorKeyHeader)
{
@@ -92,6 +93,23 @@ public void setOutboundHeaders(Set<String> 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.
* <p>
* Set this to false to avoid getting Basic auth dialogue popups in browsers
* when using the public API directly, for example.
*
* @see <a href="https://issues.alfresco.com/jira/browse/REPO-2575">REPO-2575</a>
* @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);
}
}
}
@@ -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
@@ -95,6 +95,7 @@
<property name="remoteUserMapper">
<ref bean="RemoteUserMapper" />
</property>
<property name="useBasicAuth" value="${alfresco.restApi.basicAuthScheme}"/>
</bean>
<bean id="apiBootstrapBean" class="org.alfresco.rest.framework.core.ApiBootstrap">
@@ -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 @@
{
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).

0 comments on commit dfb270a

Please sign in to comment.