Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse UUID from auth header for anonymous users #123

Merged
merged 4 commits into from
Feb 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions simplq/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>1.30.11</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,20 @@
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.auth0.jwt.exceptions.JWTVerificationException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import me.simplq.exceptions.SQAccessDeniedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
Expand All @@ -36,62 +32,69 @@
public class AuthenticationFilter implements Filter {

private static final String UNAUTHORIZED = "Unauthorized";
private static final String INVALID_TOKEN = "Invalid auth token";
private final GoogleIdTokenVerifier verifier;
private static final String ANONYMOUS_HEADER_START_WITH = "Anonymous ";
private static final String BEARER_HEADER_START_WITH = "Bearer ";
private final LoggedInUserInfo loggedInUserInfo;
private final JwkProvider provider;
private static final Logger log = LoggerFactory.getLogger(AuthenticationFilter.class);

// TODO Remove google stuff
@Autowired
AuthenticationFilter(
LoggedInUserInfo loggedInUserInfo,
@Value("#{'${google.auth.clientIds}'.split(',')}") List<String> clientIds,
@Value("${auth0.jkws.url}") String keyUrl)
AuthenticationFilter(LoggedInUserInfo loggedInUserInfo, @Value("${auth0.jkws.url}") String keyUrl)
throws MalformedURLException {
this.loggedInUserInfo = loggedInUserInfo;
verifier =
new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory())
.setAudience(clientIds)
.build();
provider = new UrlJwkProvider(new URL(keyUrl));
}

private void authenticate(String authHeaderVal) {
/**
* Sets the User ID into the request scoped loggedInUserInfo and return incase of authenticated
* request
*
* @throws SQAccessDeniedException in case the request can't be authenticated.
* @param authHeaderVal auth header value
*/
public void authenticate(String authHeaderVal) {
if (StringUtils.isEmpty(authHeaderVal)) {
throw new SQAccessDeniedException(UNAUTHORIZED);
}
var token = authHeaderVal.replaceFirst("^Bearer ", "");
if (StringUtils.isEmpty(token)) {
throw new SQAccessDeniedException(UNAUTHORIZED);

// TODO: Remove after main site is updated to reflect the new auth changes for anonymous device
// ID.
if ("Bearer anonymous".equals(authHeaderVal)) {
loggedInUserInfo.setUserId("anonymous");
return;
}
if (token.equals("anonymous")) {
loggedInUserInfo.setUserId(token);

if (authHeaderVal.startsWith(BEARER_HEADER_START_WITH)) {
bearerAuth(authHeaderVal);
return;
}

// If the header starts with "Anonymous " the remaining portion is blindly set as the user ID.
// User ID in this case is assumed to be a UUID generated by the front end. This is required to
// let unregistered users use all the functionalities as usual.
if (authHeaderVal.startsWith(ANONYMOUS_HEADER_START_WITH)) {
loggedInUserInfo.setUserId(authHeaderVal.replaceFirst(ANONYMOUS_HEADER_START_WITH, ""));
return;
}

throw new SQAccessDeniedException(UNAUTHORIZED);
}

private void bearerAuth(String authHeaderVal) {
var token = authHeaderVal.replaceFirst(BEARER_HEADER_START_WITH, "");
if (StringUtils.isEmpty(token)) {
throw new SQAccessDeniedException(UNAUTHORIZED);
}

try {
var jwt = JWT.decode(token);
var jwk = provider.get(jwt.getKeyId());
var algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
algorithm.verify(jwt);
loggedInUserInfo.setUserId(jwt.getClaim("sub").asString());
} catch (SignatureVerificationException | JwkException ex) {
// At this point we should return unauthorized. Checking google tokens for temporary
// compatibility.
// throw new SQAccessDeniedException("Invalid authorization token");

GoogleIdToken idToken = null;

try {
idToken = verifier.verify(token);
} catch (GeneralSecurityException e) {
throw new SQAccessDeniedException(UNAUTHORIZED);
} catch (IOException e) {
throw new SQAccessDeniedException(INVALID_TOKEN);
}
if (idToken == null) {
throw new SQAccessDeniedException(INVALID_TOKEN);
}
loggedInUserInfo.setUserId(idToken.getPayload().getSubject());
} catch (JWTVerificationException | JwkException ex) {
log.error("Auth0 authentication failed", ex);
throw new SQAccessDeniedException(UNAUTHORIZED);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package me.simplq.controller.advices;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.net.MalformedURLException;
import me.simplq.exceptions.SQAccessDeniedException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class AuthenticationFilterTest {
private static final String keyUrl = "https://simplq.us.auth0.com/.well-known/jwks.json";

@ParameterizedTest
@ValueSource(strings = {"", "invalid header", "invalid-header", "Bearer invalid-header"})
void denyBadHeaderValues(String testHeaderValue) throws MalformedURLException {
var authFilter = new AuthenticationFilter(new LoggedInUserInfo(), keyUrl);
// Deny bad bearer token
assertThrows(SQAccessDeniedException.class, () -> authFilter.authenticate(testHeaderValue));
}

@Test
void denyNullHeaderValues() throws MalformedURLException {
var authFilter = new AuthenticationFilter(new LoggedInUserInfo(), keyUrl);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A unit test should test only one unit of work. Here you test 6. So we can use @ParameterizedTestannotation and the method will look like this:

   @ParameterizedTest
   @ValueSource(strings = {null, "", "invalid header"}) //and so on
   void denyBadHeaderValues(String headerValue) throws MalformedURLException { 
        var authFilter = new AuthenticationFilter(new LoggedInUserInfo(), keyUrl);
       assertThrows(SQAccessDeniedException.class, () -> authFilter.authenticate(headerValue));
  }

// Deny Null
assertThrows(SQAccessDeniedException.class, () -> authFilter.authenticate(null));
}

@Test
void setAnonymousUserIdentity() throws MalformedURLException {
var loggedInUserInfo = new LoggedInUserInfo();
var authFilter = new AuthenticationFilter(loggedInUserInfo, keyUrl);
authFilter.authenticate("Anonymous anonymous-test-id");
assertEquals("anonymous-test-id", loggedInUserInfo.getUserId());

// Check backward compatibility, should be removed once frontend is updated on main site
assertDoesNotThrow(() -> authFilter.authenticate("Bearer anonymous"));
}
}