package org.dcache.gplazma.oidc;
import com.google.common.cache.LoadingCache;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.dcache.auth.BearerTokenCredential;
import org.dcache.auth.FullNamePrincipal;
import org.dcache.auth.OidcSubjectPrincipal;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
import java.io.IOException;
import java.security.Principal;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import org.dcache.auth.EmailAddressPrincipal;
import org.dcache.gplazma.AuthenticationException;
import org.dcache.gplazma.oidc.exceptions.OidcException;
import org.dcache.gplazma.oidc.helpers.JsonHttpClient;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertThat;
public class OidcAuthPluginTest
{
private static final String OIDC_PROPERTY_NAME = "gplazma.oidc.hostnames";
private Properties givenConfiguration = new Properties();
private LoadingCache cache;
private JsonHttpClient httpClient;
@BeforeClass
public static void init() throws Exception
{
}
@Before
public void setUp() throws Exception
{
cache = Mockito.mock(LoadingCache.class);
httpClient = Mockito.mock(JsonHttpClient.class);
}
@After
public void tearDown() throws Exception
{
}
@Test(expected = IllegalArgumentException.class)
public void shouldFailWithEmptyOidcHostList() throws Exception
{
givenConfig(" ");
whenOidcPluginCreated();
}
@Test(expected = IllegalArgumentException.class)
public void shouldFailWithInvalidHostnames() throws Exception
{
givenConfig(" \" ");
whenOidcPluginCreated();
}
@Test(expected = AuthenticationException.class)
public void shouldFailWithNoCredential() throws Exception
{
givenConfig("accounts.google.com idc-iam.example.org");
whenOidcPluginCalledWithNoCredentials();
}
@Test(expected = AuthenticationException.class)
public void shouldFailWhenNoDiscoveryDoc() throws Exception
{
givenConfig("accounts.google.com idc-iam.example.org");
whenOidcPluginCalledWith(
withExecutionException(),
withUserInfo("{}"),
withBearerToken(null));
}
@Test(expected = AuthenticationException.class)
public void shouldFailWhenHttpGetUserInfoFails() throws Exception
{
givenConfig("accounts.google.com idc-iam.example.org");
whenOidcPluginCalledWith(
withDiscoveryDoc("{ \"userinfo_endpoint\":\"https://www.googleapis.com/oauth2/v3/userinfo\" }"),
withIOException(),
withBearerToken(null));
}
@Test(expected = AuthenticationException.class)
public void shouldFailWhenInvalidTokenOneProvider() throws Exception
{
givenConfig("accounts.google.com");
whenOidcPluginCalledWith(
withDiscoveryDoc("{ \"userinfo_endpoint\":\"https://www.googleapis.com/oauth2/v3/userinfo\" }"),
withUserInfo("{\"error\":\"invalid_token\",\"error_description\":\"Invalid Credentials\"}"),
withBearerToken("thisisnotatoken"));
}
@Test(expected = AuthenticationException.class)
public void shouldFailWhenInvalidToken() throws Exception
{
givenConfig("accounts.google.com idc-iam.example.org");
whenOidcPluginCalledWith(
withDiscoveryDoc("{ \"userinfo_endpoint\":\"https://www.googleapis.com/oauth2/v3/userinfo\" }"),
withUserInfo("{\"error\":\"invalid_token\",\"error_description\":\"Invalid Credentials\"}"),
withBearerToken("thisisnotatoken"));
}
@Test(expected = AuthenticationException.class)
public void shouldFailWhenNoOidcSubject() throws Exception
{
givenConfig("accounts.google.com idc-iam.example.org");
whenOidcPluginCalledWith(
withDiscoveryDoc("{ \"userinfo_endpoint\":\"https://www.googleapis.com/oauth2/v3/userinfo\" }"),
withUserInfo("{\"nosub\":\"\"}"),
withBearerToken("validtoken"));
}
@Test
public void successWhenValidToken() throws Exception
{
givenConfig("accounts.google.com idc-iam.example.org");
Set<Principal> principals =
whenOidcPluginCalledWith(
withDiscoveryDoc("{ \"userinfo_endpoint\":\"https://www.googleapis.com/oauth2/v3/userinfo \"}"),
withUserInfo(new StringBuilder()
.append("{\"sub\":\"214234823942934792371\",")
.append("\"name\":\"Kermit The Frog\", ")
.append("\"given_name\": \"Kermit The\", ")
.append("\"family_name\": \"Frog\", ")
.append("\"picture\": \"https://lh3.googleusercontent.com/gjworasdfjasgjdlsjvlsjlv/photo.jpg\", ")
.append("\"email\": \"kermit.the.frog@email.com\", ")
.append("\"email_verified\": true } ")
.toString()),
withBearerToken("validtoken"));
assertThat(principals, hasSubject("214234823942934792371"));
assertThat(principals, hasFullName("Kermit The", "Frog", "Kermit The Frog"));
assertThat(principals, hasEmail("kermit.the.frog@email.com"));
}
/*-------------------------------- Helpers --------------------------------------*/
private void givenConfig(String config)
{
givenConfiguration.put(OIDC_PROPERTY_NAME, config);
}
private Set<Principal> whenOidcPluginCalledWith(JsonNode discoveryDoc,
JsonNode userInfo,
BearerTokenCredential token)
throws ExecutionException, IOException, AuthenticationException
{
OidcAuthPlugin plugin = new OidcAuthPlugin(givenConfiguration, httpClient, cache);
Mockito.doReturn(discoveryDoc).when(cache).get(Mockito.anyString());
Mockito.doReturn(userInfo).when(httpClient).doGetWithToken(Mockito.anyString(), Mockito.anyString());
Set<Object> priv = new HashSet<>();
Set<Principal> principals = new HashSet<>();
if (token != null) priv.add(token);
plugin.authenticate(new HashSet<>(), priv, principals);
return principals;
}
private void whenOidcPluginCreated() {
OidcAuthPlugin plugin = new OidcAuthPlugin(givenConfiguration, httpClient, cache);
}
private void whenOidcPluginCalledWithNoCredentials() throws AuthenticationException {
whenPluginCreated();
}
private Set<Principal> whenOidcPluginCalledWith(JsonNode discoveryDoc,
IOException e,
BearerTokenCredential token)
throws ExecutionException, IOException, AuthenticationException
{
Mockito.doReturn(discoveryDoc).when(cache).get(Mockito.anyString());
Mockito.doThrow(e).when(httpClient).doGetWithToken(Mockito.anyString(), Mockito.anyString());
return whenPluginCreated(token);
}
private Set<Principal> whenOidcPluginCalledWith(ExecutionException e,
JsonNode userInfo,
BearerTokenCredential token)
throws ExecutionException, IOException, AuthenticationException, OidcException
{
Mockito.doThrow(e).when(cache).get(Mockito.anyString());
Mockito.doReturn(userInfo).when(httpClient).doGetWithToken(Mockito.anyString(), Mockito.anyString());
return whenPluginCreated(token);
}
private Set<Principal> whenPluginCreated() throws AuthenticationException {
return this.whenPluginCreated(null);
}
private Set<Principal> whenPluginCreated(BearerTokenCredential token) throws AuthenticationException {
OidcAuthPlugin plugin = new OidcAuthPlugin(givenConfiguration, httpClient, cache);
Set<Object> priv = new HashSet<>();
Set<Principal> principals = new HashSet<>();
if (token != null) priv.add(token);
plugin.authenticate(new HashSet<>(), priv, principals);
return principals;
}
private IOException withIOException() {
return Mockito.mock(IOException.class);
}
private ExecutionException withExecutionException() {
return Mockito.mock(ExecutionException.class);
}
private JsonNode withDiscoveryDoc(String json) throws IOException {
return new ObjectMapper().readTree(json);
}
private JsonNode withUserInfo(String json) throws IOException {
return new ObjectMapper().readTree(json);
}
private BearerTokenCredential withBearerToken(String token) throws IOException {
return (token == null) ? null : new BearerTokenCredential(token);
}
public static Matcher<Iterable<? super OidcSubjectPrincipal>> hasSubject(String dn)
{
return hasItem(new OidcSubjectPrincipal(dn));
}
public static Matcher<Iterable<? super EmailAddressPrincipal>> hasEmail(String email)
{
return hasItem(new EmailAddressPrincipal(email));
}
public static Matcher<Iterable<? super FullNamePrincipal>> hasFullName(String givenName,
String familyName, String fullName)
{
if (fullName != null && !fullName.isEmpty()) {
return hasItem(new FullNamePrincipal(fullName));
} else {
return hasItem(new FullNamePrincipal(givenName, familyName));
}
}
}