Skip to content

Commit cacd550

Browse files
authored
Include WWW-Authenticate header with auth failure (AthenZ#590)
1 parent 5ebc812 commit cacd550

File tree

21 files changed

+365
-31
lines changed

21 files changed

+365
-31
lines changed

libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/Authority.java

+9
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ default CredSource getCredSource() {
5656
*/
5757
String getHeader();
5858

59+
/**
60+
* @return the string to be returned as the value for WWW-Authenticate header:
61+
* WWW-Authenticate = "WWW-Authenticate" ":" 1#challenge
62+
* in case all authorities fail to authenticate a request.
63+
*/
64+
default String getAuthenticateChallenge() {
65+
return null;
66+
}
67+
5968
/**
6069
* @return a boolean flag indicating whether or not authenticated principals
6170
* by this authority are allowed to be "authorized" to make changes. If this

libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/impl/CertificateAuthority.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@
3232
public class CertificateAuthority implements Authority {
3333

3434
private static final Logger LOG = LoggerFactory.getLogger(CertificateAuthority.class);
35-
private static final String ATHENZ_PROP_EXCLUDED_PRINCIPALS = "athenz.auth.certificate.excluded_principals";
3635

36+
private static final String ATHENZ_PROP_EXCLUDED_PRINCIPALS = "athenz.auth.certificate.excluded_principals";
37+
private static final String ATHENZ_AUTH_CHALLENGE = "AthenzX509Certificate realm=\"athenz\"";
3738
private Set<String> excludedPrincipalSet = null;
3839

3940
@Override
@@ -55,6 +56,11 @@ public String getHeader() {
5556
return null;
5657
}
5758

59+
@Override
60+
public String getAuthenticateChallenge() {
61+
return ATHENZ_AUTH_CHALLENGE;
62+
}
63+
5864
@Override
5965
public Principal authenticate(String creds, String remoteAddr, String httpMethod, StringBuilder errMsg) {
6066
return null;

libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/impl/KerberosAuthority.java

+10-4
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ public class KerberosAuthority implements Authority {
4141

4242
private static final Logger LOG = LoggerFactory.getLogger(KerberosAuthority.class);
4343

44-
static final String KRB_AUTH_HEADER = "Authorization";
45-
static final String KRB_PROP_SVCPRPL = "athenz.auth.kerberos.service_principal";
46-
static final String KRB_PROP_KEYTAB = "athenz.auth.kerberos.keytab_location";
47-
static final String KRB_PROP_DEBUG = "athenz.auth.kerberos.debug";
44+
static final String KRB_AUTH_HEADER = "Authorization";
45+
static final String KRB_AUTH_CHALLENGE = "Negotiate";
46+
static final String KRB_PROP_SVCPRPL = "athenz.auth.kerberos.service_principal";
47+
static final String KRB_PROP_KEYTAB = "athenz.auth.kerberos.keytab_location";
48+
static final String KRB_PROP_DEBUG = "athenz.auth.kerberos.debug";
4849

4950
// This is used if there is a jaas.conf. The jaas.conf path is specified by the system property
5051
// java.security.auth.login.config
@@ -229,6 +230,11 @@ public String getHeader() {
229230
return KRB_AUTH_HEADER;
230231
}
231232

233+
@Override
234+
public String getAuthenticateChallenge() {
235+
return KRB_AUTH_CHALLENGE;
236+
}
237+
232238
/**
233239
* Verify the credentials and if valid return the corresponding Principal, null otherwise.
234240
* @param creds the credentials (i.e. cookie, token, secret) that will identify the principal.

libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/impl/PrincipalAuthority.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public class PrincipalAuthority implements Authority, AuthorityKeyStore {
3939
private static final String ATHENZ_PROP_USER_DOMAIN = "athenz.user_domain";
4040

4141
public static final String HTTP_HEADER = "Athenz-Principal-Auth";
42+
public static final String ATHENZ_AUTH_CHALLENGE = "AthenzPrincipalToken realm=\"athenz\"";
4243
public static final String ATHENZ_PROP_PRINCIPAL_HEADER = "athenz.auth.principal.header";
4344

4445
private static final Logger LOG = LoggerFactory.getLogger(PrincipalAuthority.class);
@@ -83,6 +84,11 @@ public String getHeader() {
8384
return headerName;
8485
}
8586

87+
@Override
88+
public String getAuthenticateChallenge() {
89+
return ATHENZ_AUTH_CHALLENGE;
90+
}
91+
8692
@Override
8793
public Principal authenticate(String signedToken, String remoteAddr, String httpMethod,
8894
StringBuilder errMsg) {
@@ -167,11 +173,6 @@ public Principal authenticate(String signedToken, String remoteAddr, String http
167173

168174
SimplePrincipal princ = (SimplePrincipal) SimplePrincipal.create(tokenDomain,
169175
tokenName, signedToken, serviceToken.getTimestamp(), this);
170-
if (princ == null) {
171-
errMsg.append("PrincipalAuthority:authenticate: Unable to create principal");
172-
LOG.error(errMsg.toString());
173-
return null;
174-
}
175176
princ.setUnsignedCreds(serviceToken.getUnsignedToken());
176177
princ.setAuthorizedService(authorizedServiceName);
177178
princ.setOriginalRequestor(serviceToken.getOriginalRequestor());

libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/impl/RoleAuthority.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class RoleAuthority implements Authority, AuthorityKeyStore {
3737
static final String ATHENZ_PROP_USER_DOMAIN = "athenz.user_domain";
3838

3939
public static final String HTTP_HEADER = "Athenz-Role-Auth";
40+
public static final String ATHENZ_AUTH_CHALLENGE = "AthenzRoleToken realm=\"athenz\"";
4041
public static final String ATHENZ_PROP_ROLE_HEADER = "athenz.auth.role.header";
4142

4243
private int allowedOffset;
@@ -71,6 +72,11 @@ public String getHeader() {
7172
return headerName;
7273
}
7374

75+
@Override
76+
public String getAuthenticateChallenge() {
77+
return ATHENZ_AUTH_CHALLENGE;
78+
}
79+
7480
@Override
7581
public Principal authenticate(String signedToken, String remoteAddr, String httpMethod, StringBuilder errMsg) {
7682

@@ -133,12 +139,11 @@ public Principal authenticate(String signedToken, String remoteAddr, String http
133139

134140
// all the role members in Athenz are normalized to lower case so we need to make
135141
// sure our principal's name and domain are created with lower case as well
142+
// we have verified that our token already includes valid roles
136143

137144
SimplePrincipal princ = (SimplePrincipal) SimplePrincipal.create(roleToken.getDomain().toLowerCase(),
138145
signedToken, roleToken.getRoles(), this);
139-
if (princ != null) {
140-
princ.setUnsignedCreds(roleToken.getUnsignedToken());
141-
}
146+
princ.setUnsignedCreds(roleToken.getUnsignedToken());
142147
return princ;
143148
}
144149

libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/impl/SimpleServiceIdentityProvider.java

+1-3
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,7 @@ public Principal getIdentity(String domainName, String serviceName) {
114114
SimplePrincipal principal = (SimplePrincipal) SimplePrincipal.create(domainName,
115115
serviceName, token.getSignedToken(), System.currentTimeMillis() / 1000,
116116
authority);
117-
if (principal != null) {
118-
principal.setUnsignedCreds(token.getUnsignedToken());
119-
}
117+
principal.setUnsignedCreds(token.getUnsignedToken());
120118
return principal;
121119
}
122120

libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/impl/UserAuthority.java

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class UserAuthority implements Authority {
3535

3636
private static final Logger LOG = LoggerFactory.getLogger(UserAuthority.class);
3737
static final String ATHENZ_PROP_PAM_SERVICE_NAME = "athenz.auth.user.pam_service_name";
38+
public static final String ATHENZ_AUTH_CHALLENGE = "Basic realm=\"athenz\"";
3839

3940
String serviceName;
4041
private PAM pam = null;
@@ -57,6 +58,11 @@ public String getHeader() {
5758
return "Authorization";
5859
}
5960

61+
@Override
62+
public String getAuthenticateChallenge() {
63+
return ATHENZ_AUTH_CHALLENGE;
64+
}
65+
6066
/*
6167
* we don't want the user to keep specifying their username and
6268
* password as part of the request. instead, the user must first

libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/KerberosToken.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ public boolean validate(Subject serviceSubject, StringBuilder errMsg) {
8686
}
8787
userName = userName.substring(0, index);
8888
}
89-
9089
return true;
90+
9191
} catch (PrivilegedActionException paexc) {
9292
if (errMsg == null) {
9393
errMsg = new StringBuilder(512);

libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/CertificateAuthorityTest.java

+6
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,10 @@ public void testAuthenciateInvalidArray() {
110110
principal = authority.authenticate(certs, errMsg);
111111
assertNull(principal);
112112
}
113+
114+
@Test
115+
public void testGetAuthenticateChallenge() {
116+
CertificateAuthority authority = new CertificateAuthority();
117+
assertEquals(authority.getAuthenticateChallenge(), "AthenzX509Certificate realm=\"athenz\"");
118+
}
113119
}

libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/KerberosAuthorityTest.java

+6
Original file line numberDiff line numberDiff line change
@@ -366,4 +366,10 @@ public void testIsTargetPrincipalIlligal() {
366366

367367
assertFalse(check.isTargetPrincipal(null,null));
368368
}
369+
370+
@Test
371+
public void testGetAuthenticateChallenge() {
372+
KerberosAuthority krbAuthority = new KerberosAuthority();
373+
assertEquals(krbAuthority.getAuthenticateChallenge(), "Negotiate");
374+
}
369375
}

libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/PrincipalAuthorityTest.java

+6
Original file line numberDiff line numberDiff line change
@@ -592,4 +592,10 @@ public void testPrincipalAuthorityAuthenticateIlligal() throws CryptoException {
592592
Principal check = serviceAuthority.authenticate(t, "10", "10", null);
593593
assertNull(check);
594594
}
595+
596+
@Test
597+
public void testGetAuthenticateChallenge() {
598+
PrincipalAuthority serviceAuthority = new PrincipalAuthority();
599+
assertEquals(serviceAuthority.getAuthenticateChallenge(), "AthenzPrincipalToken realm=\"athenz\"");
600+
}
595601
}

libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/RoleAuthorityTest.java

+6
Original file line numberDiff line numberDiff line change
@@ -393,4 +393,10 @@ public void testInitialize() throws NoSuchFieldException, SecurityException,
393393
assertEquals(m,300);
394394
assertEquals(roleAuthority.userDomain,"user");
395395
}
396+
397+
@Test
398+
public void testGetAuthenticateChallenge() {
399+
RoleAuthority roleAuthority = new RoleAuthority();
400+
assertEquals(roleAuthority.getAuthenticateChallenge(), "AthenzRoleToken realm=\"athenz\"");
401+
}
396402
}

libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/SimpleServiceIdentityProviderTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ public void testConstructorWithAuthority() {
120120
assertEquals(provider.getAuthority(), authority);
121121

122122
SimpleServiceIdentityProvider provider2 = new SimpleServiceIdentityProvider("coretech",
123-
"athenz", key, "1", 3600);
123+
"athenz", key, "1", 3900);
124124
assertNotEquals(provider2.getAuthority(), authority);
125125
provider2.setAuthority(authority);
126+
provider2.setTokenTimeout(3600);
126127
assertEquals(provider2.getAuthority(), authority);
127128
}
128129
}

libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/UserAuthorityTest.java

+6
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,10 @@ public void testAuthenticateException() throws PAMException {
8383
principal = userAuthority.authenticate("Basic ", "10.72.118.45", "GET", null);
8484
assertNull(principal);
8585
}
86+
87+
@Test
88+
public void testGetAuthenticateChallenge() {
89+
UserAuthority userAuthority = new UserAuthority();
90+
assertEquals(userAuthority.getAuthenticateChallenge(), "Basic realm=\"athenz\"");
91+
}
8692
}

libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/rest/Http.java

+25-6
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
*/
1616
package com.yahoo.athenz.common.server.rest;
1717

18+
import java.util.HashSet;
1819
import java.util.List;
1920
import java.util.ArrayList;
2021
import java.security.cert.X509Certificate;
22+
import java.util.Set;
2123

2224
import javax.servlet.http.HttpServletRequest;
2325

@@ -33,9 +35,11 @@ public class Http {
3335

3436
private static final Logger LOG = LoggerFactory.getLogger(Http.class);
3537

38+
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
3639
public static final String INVALID_CRED_ATTR = "com.yahoo.athenz.auth.credential.error";
40+
public static final String AUTH_CHALLENGES = "com.yahoo.athenz.auth.credential.challenges";
3741
public static final String JAVAX_CERT_ATTR = "javax.servlet.request.X509Certificate";
38-
42+
3943
public static class AuthorityList {
4044
List<Authority> authorities;
4145

@@ -66,7 +70,7 @@ static String getCookieValue(HttpServletRequest hreq, String name) {
6670
return null;
6771
}
6872

69-
private static String authenticatingCredentials(HttpServletRequest request,
73+
static String authenticatingCredentials(HttpServletRequest request,
7074
Authority authority) {
7175
final String header = authority.getHeader();
7276
if (header == null) {
@@ -91,12 +95,14 @@ public static Principal authenticate(HttpServletRequest request,
9195
}
9296

9397
StringBuilder authErrMsg = new StringBuilder(512);
98+
Set<String> authChallenges = null;
9499
for (Authority authority : authorities.authorities) {
95100
Principal principal = null;
96101
StringBuilder errMsg = new StringBuilder(512);
97102
switch (authority.getCredSource()) {
98103
case HEADER:
99104
String creds = authenticatingCredentials(request, authority);
105+
100106
if (creds != null) {
101107
principal = authority.authenticate(creds, ServletRequestUtil.getRemoteAddress(request),
102108
request.getMethod(), errMsg);
@@ -119,7 +125,15 @@ public static Principal authenticate(HttpServletRequest request,
119125
if (principal != null) {
120126
return principal;
121127
}
122-
128+
129+
final String challenge = authority.getAuthenticateChallenge();
130+
if (challenge != null) {
131+
if (authChallenges == null) {
132+
authChallenges = new HashSet<>();
133+
}
134+
authChallenges.add(challenge);
135+
}
136+
123137
// otherwise if we have a specific error message from an authority
124138
// then we'll keep it in case all other authorities also fail and
125139
// we need to log the reason for failure
@@ -151,6 +165,14 @@ public static Principal authenticate(HttpServletRequest request,
151165
LOG.error("authenticate: No credentials provided");
152166
}
153167

168+
// if we have challenges specified, we're going to set it as a request
169+
// attribute and let the caller decide if they want to add it to the
170+
// response as a header in its context handler
171+
172+
if (authChallenges != null) {
173+
request.setAttribute(AUTH_CHALLENGES, String.join(", ", authChallenges));
174+
}
175+
154176
throw new ResourceException(ResourceException.UNAUTHORIZED, "Invalid credentials");
155177
}
156178

@@ -165,9 +187,6 @@ public static String authorizedUser(HttpServletRequest request,
165187
String resource, String otherDomain) {
166188
Principal principal = authenticate(request, authorities);
167189
authorize(authorizer, principal, action, resource, otherDomain);
168-
if (principal == null) {
169-
return null;
170-
}
171190
return principal.getFullName();
172191
}
173192

libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/rest/ResourceContext.java

+25
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.yahoo.athenz.auth.Principal;
2323

2424
public class ResourceContext {
25+
2526
private final HttpServletRequest request;
2627
private final HttpServletResponse response;
2728
private final Http.AuthorityList authorities;
@@ -92,4 +93,28 @@ public void authorize(String action, String resource, String trustedDomain) {
9293
principal = authenticate();
9394
Http.authorize(authorizer, principal, action, resource, trustedDomain);
9495
}
96+
97+
98+
/**
99+
* If requested include the WWW-Authenticate challenge response
100+
* header for this request. This is only done if the exception
101+
* was thrown for invalid credentials.
102+
* @param exc ResourceException that was thrown when calling authenticate
103+
*/
104+
public void sendAuthenticateChallenges(ResourceException exc) {
105+
106+
// first check to see if this is an auth failure and if
107+
// that's the case include the WWW-Authenticate challenge
108+
109+
if (exc.getCode() != ResourceException.UNAUTHORIZED) {
110+
return;
111+
}
112+
113+
Object authChallenges = request.getAttribute(Http.AUTH_CHALLENGES);
114+
if (authChallenges == null) {
115+
return;
116+
}
117+
118+
response.addHeader(Http.WWW_AUTHENTICATE, authChallenges.toString());
119+
}
95120
}

0 commit comments

Comments
 (0)