diff options
Diffstat (limited to 'odl-aaa-moon/aaa/aaa-authn-basic')
4 files changed, 338 insertions, 0 deletions
diff --git a/odl-aaa-moon/aaa/aaa-authn-basic/pom.xml b/odl-aaa-moon/aaa/aaa-authn-basic/pom.xml new file mode 100644 index 00000000..47562896 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-basic/pom.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authn-basic</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + <!-- Testing Dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Activator>org.opendaylight.aaa.basic.Activator</Bundle-Activator> + </instructions> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/Activator.java b/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/Activator.java new file mode 100644 index 00000000..bd57c9d3 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/Activator.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.basic; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.TokenAuth; +import org.osgi.framework.BundleContext; + +public class Activator extends DependencyActivatorBase { + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + manager.add(createComponent() + .setInterface(new String[] { TokenAuth.class.getName() }, null) + .setImplementation(HttpBasicAuth.class) + .add(createServiceDependency().setService(CredentialAuth.class).setRequired(true))); + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/HttpBasicAuth.java b/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/HttpBasicAuth.java new file mode 100644 index 00000000..eff47e63 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/HttpBasicAuth.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.basic; + +import com.sun.jersey.core.util.Base64; +import java.util.List; +import java.util.Map; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.PasswordCredentialBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.PasswordCredentials; +import org.opendaylight.aaa.api.TokenAuth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An HTTP Basic authenticator. Note that this is provided as a Hydrogen + * backward compatible authenticator, but usage of this authenticator or HTTP + * Basic Authentication is highly discouraged due to its vulnerability. + * + * To obtain a token using the HttpBasicAuth Strategy, add a header to your HTTP + * request in the form: + * <code>Authorization: Basic BASE_64_ENCODED_CREDENTIALS</code> + * + * Where <code>BASE_64_ENCODED_CREDENTIALS</code> is the base 64 encoded value + * of the user's credentials in the following form: <code>user:password</code> + * + * For example, assuming the user is "admin" and the password is "admin": + * <code>Authorization: Basic YWRtaW46YWRtaW4=</code> + * + * @author liemmn + * + */ +public class HttpBasicAuth implements TokenAuth { + + public static final String AUTH_HEADER = "Authorization"; + + public static final String AUTH_SEP = ":"; + + public static final String BASIC_PREFIX = "Basic "; + + // TODO relocate this constant + public static final String DEFAULT_DOMAIN = "sdn"; + + /** + * username and password + */ + private static final int NUM_HEADER_CREDS = 2; + + /** + * username, password and domain + */ + private static final int NUM_TOKEN_CREDS = 3; + + private static final Logger LOG = LoggerFactory.getLogger(HttpBasicAuth.class); + + volatile CredentialAuth<PasswordCredentials> credentialAuth; + + private static boolean checkAuthHeaderFormat(final String authHeader) { + return (authHeader != null && authHeader.startsWith(BASIC_PREFIX)); + } + + private static String extractAuthHeader(final Map<String, List<String>> headers) { + return headers.get(AUTH_HEADER).get(0); + } + + private static String[] extractCredentialArray(final String authHeader) { + return new String(Base64.base64Decode(authHeader.substring(BASIC_PREFIX.length()))) + .split(AUTH_SEP); + } + + private static boolean verifyCredentialArray(final String[] creds) { + return (creds != null && creds.length == NUM_HEADER_CREDS); + } + + private static String[] addDomainToCredentialArray(final String[] creds) { + String newCredentialArray[] = new String[NUM_TOKEN_CREDS]; + System.arraycopy(creds, 0, newCredentialArray, 0, creds.length); + newCredentialArray[2] = DEFAULT_DOMAIN; + return newCredentialArray; + } + + private static Authentication generateAuthentication( + CredentialAuth<PasswordCredentials> credentialAuth, final String[] creds) + throws ArrayIndexOutOfBoundsException { + final PasswordCredentials pc = new PasswordCredentialBuilder().setUserName(creds[0]) + .setPassword(creds[1]).setDomain(creds[2]).build(); + final Claim claim = credentialAuth.authenticate(pc); + return new AuthenticationBuilder(claim).build(); + } + + @Override + public Authentication validate(final Map<String, List<String>> headers) + throws AuthenticationException { + if (headers.containsKey(AUTH_HEADER)) { + final String authHeader = extractAuthHeader(headers); + if (checkAuthHeaderFormat(authHeader)) { + // HTTP Basic Auth + String[] creds = extractCredentialArray(authHeader); + // If no domain was supplied then use the default one, which is + // "sdn". + if (verifyCredentialArray(creds)) { + creds = addDomainToCredentialArray(creds); + } + // Assumes correct formatting in form Base64("user:password"). + // Throws an exception if an unknown format is used. + try { + return generateAuthentication(this.credentialAuth, creds); + } catch (ArrayIndexOutOfBoundsException e) { + final String message = "Login Attempt in Bad Format." + + " Please provide user:password in Base64 format."; + LOG.info(message); + throw new AuthenticationException(message); + } + } + } + return null; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-basic/src/test/java/org/opendaylight/aaa/basic/HttpBasicAuthTest.java b/odl-aaa-moon/aaa/aaa-authn-basic/src/test/java/org/opendaylight/aaa/basic/HttpBasicAuthTest.java new file mode 100644 index 00000000..4ee439df --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-basic/src/test/java/org/opendaylight/aaa/basic/HttpBasicAuthTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.basic; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sun.jersey.core.util.Base64; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.PasswordCredentialBuilder; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.CredentialAuth; + +public class HttpBasicAuthTest { + private static final String USERNAME = "admin"; + private static final String PASSWORD = "admin"; + private static final String DOMAIN = "sdn"; + private HttpBasicAuth auth; + + @SuppressWarnings("unchecked") + @Before + public void setup() { + auth = new HttpBasicAuth(); + auth.credentialAuth = mock(CredentialAuth.class); + when( + auth.credentialAuth.authenticate(new PasswordCredentialBuilder() + .setUserName(USERNAME).setPassword(PASSWORD).setDomain(DOMAIN).build())) + .thenReturn( + new ClaimBuilder().setUser("admin").addRole("admin").setUserId("123") + .build()); + when( + auth.credentialAuth.authenticate(new PasswordCredentialBuilder() + .setUserName(USERNAME).setPassword("bozo").setDomain(DOMAIN).build())) + .thenThrow(new AuthenticationException("barf")); + } + + @Test + public void testValidateOk() throws UnsupportedEncodingException { + String data = USERNAME + ":" + PASSWORD + ":" + DOMAIN; + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Authorization", + Arrays.asList("Basic " + new String(Base64.encode(data.getBytes("utf-8"))))); + Claim claim = auth.validate(headers); + assertNotNull(claim); + assertEquals(USERNAME, claim.user()); + assertEquals("admin", claim.roles().iterator().next()); + } + + @Test(expected = AuthenticationException.class) + public void testValidateBadPassword() throws UnsupportedEncodingException { + String data = USERNAME + ":bozo:" + DOMAIN; + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Authorization", + Arrays.asList("Basic " + new String(Base64.encode(data.getBytes("utf-8"))))); + auth.validate(headers); + } + + @Test(expected = AuthenticationException.class) + public void testValidateBadPasswordNoDOMAIN() throws UnsupportedEncodingException { + String data = USERNAME + ":bozo"; + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Authorization", + Arrays.asList("Basic " + new String(Base64.encode(data.getBytes("utf-8"))))); + auth.validate(headers); + } + + @Test(expected = AuthenticationException.class) + public void testBadHeaderFormatNoPassword() throws UnsupportedEncodingException { + // just provide the username + String data = USERNAME; + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Authorization", + Arrays.asList("Basic " + new String(Base64.encode(data.getBytes("utf-8"))))); + auth.validate(headers); + } + + @Test(expected = AuthenticationException.class) + public void testBadHeaderFormat() throws UnsupportedEncodingException { + // provide username: + String data = USERNAME + "$" + PASSWORD; + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Authorization", + Arrays.asList("Basic " + new String(Base64.encode(data.getBytes("utf-8"))))); + auth.validate(headers); + } +} |