diff --git a/authhub/src/main/java/com/networknt/oauth/security/LightFormAuthenticationMechanism.java b/authhub/src/main/java/com/networknt/oauth/security/LightFormAuthenticationMechanism.java new file mode 100644 index 00000000..9d3f31d6 --- /dev/null +++ b/authhub/src/main/java/com/networknt/oauth/security/LightFormAuthenticationMechanism.java @@ -0,0 +1,210 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.oauth.security; + +import com.hazelcast.core.IMap; +import com.networknt.oauth.cache.CacheStartupHookProvider; +import com.networknt.oauth.cache.model.Client; +import io.undertow.UndertowLogger; +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.idm.Account; +import io.undertow.security.idm.IdentityManager; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.form.FormData; +import io.undertow.server.handlers.form.FormDataParser; +import io.undertow.server.handlers.form.FormParserFactory; +import io.undertow.server.session.Session; +import io.undertow.util.*; + +import java.io.IOException; + +import static io.undertow.UndertowMessages.MESSAGES; + +/** + * A customized FormAuthenticationMechanism that allows authenticator and authorizer injection. + * + * @author Steve Hu + */ +public class LightFormAuthenticationMechanism implements AuthenticationMechanism { + + public static final String LOCATION_ATTRIBUTE = LightFormAuthenticationMechanism.class.getName() + ".LOCATION"; + + public static final String DEFAULT_POST_LOCATION = "/j_security_check"; + + private final String name; + private final String loginPage; + private final String errorPage; + private final String postLocation; + private final FormParserFactory formParserFactory; + private final IdentityManager identityManager; + + public LightFormAuthenticationMechanism(final String name, final String loginPage, final String errorPage) { + this(FormParserFactory.builder().build(), name, loginPage, errorPage); + } + + public LightFormAuthenticationMechanism(final String name, final String loginPage, final String errorPage, final String postLocation) { + this(FormParserFactory.builder().build(), name, loginPage, errorPage, postLocation); + } + + public LightFormAuthenticationMechanism(final FormParserFactory formParserFactory, final String name, final String loginPage, final String errorPage) { + this(formParserFactory, name, loginPage, errorPage, DEFAULT_POST_LOCATION); + } + + public LightFormAuthenticationMechanism(final FormParserFactory formParserFactory, final String name, final String loginPage, final String errorPage, final IdentityManager identityManager) { + this(formParserFactory, name, loginPage, errorPage, DEFAULT_POST_LOCATION, identityManager); + } + + public LightFormAuthenticationMechanism(final FormParserFactory formParserFactory, final String name, final String loginPage, final String errorPage, final String postLocation) { + this(formParserFactory, name, loginPage, errorPage, postLocation, null); + } + + public LightFormAuthenticationMechanism(final FormParserFactory formParserFactory, final String name, final String loginPage, final String errorPage, final String postLocation, final IdentityManager identityManager) { + this.name = name; + this.loginPage = loginPage; + this.errorPage = errorPage; + this.postLocation = postLocation; + this.formParserFactory = formParserFactory; + this.identityManager = identityManager; + } + + @SuppressWarnings("deprecation") + private IdentityManager getIdentityManager(SecurityContext securityContext) { + return identityManager != null ? identityManager : securityContext.getIdentityManager(); + } + + @Override + public AuthenticationMechanismOutcome authenticate(final HttpServerExchange exchange, + final SecurityContext securityContext) { + if (exchange.getRequestPath().endsWith(postLocation) && exchange.getRequestMethod().equals(Methods.POST)) { + return runFormAuth(exchange, securityContext); + } else { + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + } + + public AuthenticationMechanismOutcome runFormAuth(final HttpServerExchange exchange, final SecurityContext securityContext) { + final FormDataParser parser = formParserFactory.createParser(exchange); + if (parser == null) { + UndertowLogger.SECURITY_LOGGER.debug("Could not authenticate as no form parser is present"); + // TODO - May need a better error signaling mechanism here to prevent repeated attempts. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + + try { + final FormData data = parser.parseBlocking(); + final FormData.FormValue jUsername = data.getFirst("j_username"); + final FormData.FormValue jPassword = data.getFirst("j_password"); + final FormData.FormValue jClientId = data.getFirst("client_id"); + final FormData.FormValue jUserType = data.getFirst("user_type"); + if (jUsername == null || jPassword == null) { + UndertowLogger.SECURITY_LOGGER.debugf("Could not authenticate as username or password was not present in the posted result for %s", exchange); + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + final String userName = jUsername.getValue(); + final String password = jPassword.getValue(); + final String userType = jUserType.getValue(); + final String clientId = jClientId.getValue(); + + // get clientAuthClass and userType + String clientAuthClass = null; + IMap clients = CacheStartupHookProvider.hz.getMap("clients"); + Client client = clients.get(clientId); + if(client != null) { + clientAuthClass = client.getAuthenticateClass(); + } + + AuthenticationMechanismOutcome outcome = null; + LightPasswordCredential credential = new LightPasswordCredential(password.toCharArray(), clientAuthClass, userType); + try { + IdentityManager identityManager = getIdentityManager(securityContext); + Account account = identityManager.verify(userName, credential); + if (account != null) { + securityContext.authenticationComplete(account, name, true); + UndertowLogger.SECURITY_LOGGER.debugf("Authenticated user %s using for auth for %s", account.getPrincipal().getName(), exchange); + outcome = AuthenticationMechanismOutcome.AUTHENTICATED; + } else { + securityContext.authenticationFailed(MESSAGES.authenticationFailed(userName), name); + } + } finally { +// if (outcome == AuthenticationMechanismOutcome.AUTHENTICATED) { +// handleRedirectBack(exchange); +// exchange.endExchange(); +// } + return outcome != null ? outcome : AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /* + protected void handleRedirectBack(final HttpServerExchange exchange) { + final Session session = Sessions.getSession(exchange); + if (session != null) { + final String location = (String) session.removeAttribute(LOCATION_ATTRIBUTE); + if(location != null) { + exchange.addDefaultResponseListener(new DefaultResponseListener() { + @Override + public boolean handleDefaultResponse(final HttpServerExchange exchange) { + exchange.getResponseHeaders().put(Headers.LOCATION, location); + exchange.setStatusCode(StatusCodes.FOUND); + exchange.endExchange(); + return true; + } + }); + } + + } + } + */ + public ChallengeResult sendChallenge(final HttpServerExchange exchange, final SecurityContext securityContext) { + if (exchange.getRequestPath().endsWith(postLocation) && exchange.getRequestMethod().equals(Methods.POST)) { + UndertowLogger.SECURITY_LOGGER.debugf("Serving form auth error page %s for %s", loginPage, exchange); + // This method would no longer be called if authentication had already occurred. + Integer code = servePage(exchange, errorPage); + return new ChallengeResult(true, code); + } else { + UndertowLogger.SECURITY_LOGGER.debugf("Serving login form %s for %s", loginPage, exchange); + + // we need to store the URL + storeInitialLocation(exchange); + // TODO - Rather than redirecting, in order to make this mechanism compatible with the other mechanisms we need to + // return the actual error page not a redirect. + Integer code = servePage(exchange, loginPage); + return new ChallengeResult(true, code); + } + } + + protected void storeInitialLocation(final HttpServerExchange exchange) { + Session session = Sessions.getOrCreateSession(exchange); + session.setAttribute(LOCATION_ATTRIBUTE, RedirectBuilder.redirect(exchange, exchange.getRelativePath())); + } + + protected Integer servePage(final HttpServerExchange exchange, final String location) { + sendRedirect(exchange, location); + return StatusCodes.TEMPORARY_REDIRECT; + } + + + static void sendRedirect(final HttpServerExchange exchange, final String location) { + // TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better handle this. + String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location; + exchange.getResponseHeaders().put(Headers.LOCATION, loc); + } +} diff --git a/authhub/src/main/java/com/networknt/oauth/security/LightIdentityManager.java b/authhub/src/main/java/com/networknt/oauth/security/LightIdentityManager.java index df1d4945..ab7ab13c 100644 --- a/authhub/src/main/java/com/networknt/oauth/security/LightIdentityManager.java +++ b/authhub/src/main/java/com/networknt/oauth/security/LightIdentityManager.java @@ -6,6 +6,7 @@ import io.undertow.security.idm.Account; import io.undertow.security.idm.Credential; import io.undertow.security.idm.IdentityManager; +import io.undertow.security.idm.PasswordCredential; import org.ietf.jgss.GSSException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/code/src/main/java/com/networknt/oauth/code/handler/BaseWrapper.java b/code/src/main/java/com/networknt/oauth/code/handler/BaseWrapper.java index 757d5dc9..7d737497 100644 --- a/code/src/main/java/com/networknt/oauth/code/handler/BaseWrapper.java +++ b/code/src/main/java/com/networknt/oauth/code/handler/BaseWrapper.java @@ -2,6 +2,7 @@ import com.networknt.config.Config; import com.networknt.oauth.security.LightBasicAuthenticationMechanism; +import com.networknt.oauth.security.LightFormAuthenticationMechanism; import com.networknt.oauth.security.LightGSSAPIAuthenticationMechanism; import com.networknt.oauth.security.LightIdentityManager; import io.undertow.security.api.AuthenticationMechanism; @@ -67,7 +68,7 @@ protected HttpHandler addFormSecurity(final HttpHandler toWrap, final IdentityMa handler = new AuthenticationConstraintHandler(handler); final List mechanisms = new ArrayList<>(); mechanisms.add(new CachedAuthenticatedSessionMechanism()); - mechanisms.add(new FormAuthenticationMechanism("oauth2", "/login", "/error", "/oauth2/code")); + mechanisms.add(new LightFormAuthenticationMechanism("oauth2", "/login", "/error", "/oauth2/code")); handler = new AuthenticationMechanismsHandler(handler, mechanisms); handler = new SecurityInitialHandler(AuthenticationMode.PRO_ACTIVE, identityManager, handler); handler = new SessionAttachmentHandler(handler, new InMemorySessionManager("oauth2"), new SessionCookieConfig()); diff --git a/code/src/main/java/com/networknt/oauth/code/handler/Oauth2CodePostHandler.java b/code/src/main/java/com/networknt/oauth/code/handler/Oauth2CodePostHandler.java index 1e98919b..c4145883 100644 --- a/code/src/main/java/com/networknt/oauth/code/handler/Oauth2CodePostHandler.java +++ b/code/src/main/java/com/networknt/oauth/code/handler/Oauth2CodePostHandler.java @@ -6,9 +6,13 @@ import com.networknt.oauth.cache.model.Client; import com.networknt.status.Status; import com.networknt.utility.Util; +import io.undertow.UndertowLogger; +import io.undertow.security.api.AuthenticationMechanism; import io.undertow.security.api.SecurityContext; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.form.FormData; +import io.undertow.server.handlers.form.FormDataParser; import io.undertow.util.Headers; import io.undertow.util.StatusCodes; import org.slf4j.Logger; @@ -26,18 +30,16 @@ public class Oauth2CodePostHandler extends CodeAuditHandler implements LightHttp public void handleRequest(HttpServerExchange exchange) throws Exception { exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json"); - // parse all the parameters here as this is a redirected get request. - Map params = new HashMap<>(); - Map> pnames = exchange.getQueryParameters(); - for (Map.Entry> entry : pnames.entrySet()) { - String pname = entry.getKey(); - Iterator pvalues = entry.getValue().iterator(); - if(pvalues.hasNext()) { - params.put(pname, pvalues.next()); - } - } - if(logger.isDebugEnabled()) logger.debug("params", params); - String clientId = params.get("client_id"); + // get the form from the exchange + final FormData data = exchange.getAttachment(FormDataParser.FORM_DATA); + + final FormData.FormValue jClientId = data.getFirst("client_id"); + final FormData.FormValue jRedirectUri = data.getFirst("redirect_uri"); + final FormData.FormValue jState = data.getFirst("state"); + final String clientId = jClientId.getValue(); + String redirectUri = jRedirectUri == null ? null : jRedirectUri.getValue(); + final String state = jState == null ? null : jState.getValue(); + // check if the client_id is valid IMap clients = CacheStartupHookProvider.hz.getMap("clients"); Client client = clients.get(clientId); @@ -45,18 +47,6 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { setExchangeStatus(exchange, CLIENT_NOT_FOUND, clientId); processAudit(exchange); } else { - /* - String clazz = (String)client.get("authenticateClass"); - if(clazz == null) clazz = DEFAULT_AUTHENTICATE_CLASS; - Authentication auth = (Authentication)Class.forName(clazz).getConstructor().newInstance(); - String userId = auth.authenticate(exchange); - if(userId == null) { - Status status = new Status(MISSING_AUTHORIZATION_HEADER, clientId); - exchange.setStatusCode(status.getStatusCode()); - exchange.getResponseSender().send(status.toString()); - return; - } - */ final SecurityContext context = exchange.getSecurityContext(); String userId = context.getAuthenticatedAccount().getPrincipal().getName(); Set roles = context.getAuthenticatedAccount().getRoles(); @@ -67,7 +57,6 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { } // generate auth code String code = Util.getUUID(); - String redirectUri = params.get("redirect_uri"); if(redirectUri == null) { redirectUri = client.getRedirectUri(); } else { @@ -76,7 +65,6 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { CacheStartupHookProvider.hz.getMap("codes").set(code, codeMap); redirectUri = redirectUri + "?code=" + code; - String state = params.get("state"); if(state != null) { redirectUri = redirectUri + "&state=" + state; } diff --git a/code/src/test/java/com/networknt/oauth/code/handler/Oauth2CodePostHandlerTest.java b/code/src/test/java/com/networknt/oauth/code/handler/Oauth2CodePostHandlerTest.java index 7162ac8f..ab1aff79 100644 --- a/code/src/test/java/com/networknt/oauth/code/handler/Oauth2CodePostHandlerTest.java +++ b/code/src/test/java/com/networknt/oauth/code/handler/Oauth2CodePostHandlerTest.java @@ -6,8 +6,12 @@ import io.undertow.client.ClientConnection; import io.undertow.client.ClientRequest; import io.undertow.client.ClientResponse; +import io.undertow.server.handlers.form.FormEncodedDataDefinition; import io.undertow.util.Headers; import io.undertow.util.Methods; +import org.apache.hc.core5.http.copied.NameValuePair; +import org.apache.hc.core5.http.message.copied.BasicNameValuePair; +import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.slf4j.Logger; @@ -16,12 +20,16 @@ import org.xnio.OptionMap; import java.net.URI; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import static com.networknt.client.oauth.OauthHelper.encodeCredentials; + /** * Generated by swagger-codegen @@ -32,13 +40,75 @@ public class Oauth2CodePostHandlerTest { static final Logger logger = LoggerFactory.getLogger(Oauth2CodePostHandlerTest.class); + /** + * This is to simulate the incorrect credentials. It should redirect back to the login page + * to collect the user's credential. + * + * @throws Exception + */ + @Test + public void testAuthorizationCodeWrongPassword() throws Exception { + Map params = new HashMap<>(); + params.put("j_username", "admin"); + // params.put("j_password", "123456"); + params.put("j_password", "wrong"); + params.put("response_type", "code"); + params.put("user_type", "employee"); + params.put("client_id", "59f347a0-c92d-11e6-9d9d-cec0c932ce01"); + params.put("redirect_uri", "http://localhost:8080/authorization"); + + final AtomicReference reference = new AtomicReference<>(); + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("https://localhost:6881"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.create(UndertowOptions.ENABLE_HTTP2, true)).get(); + } catch (Exception e) { + throw new ClientException(e); + } + String s = Http2Client.getFormDataString(params); + try { + connection.getIoThread().execute(new Runnable() { + @Override + public void run() { + final ClientRequest request = new ClientRequest().setMethod(Methods.POST).setPath("/oauth2/code"); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(Headers.CONTENT_TYPE, FormEncodedDataDefinition.APPLICATION_X_WWW_FORM_URLENCODED); + request.getRequestHeaders().put(Headers.TRANSFER_ENCODING, "chunked"); + connection.sendRequest(request, client.createClientCallback(reference, latch, s)); + } + }); + latch.await(10, TimeUnit.SECONDS); + int statusCode = reference.get().getResponseCode(); + String body = reference.get().getAttachment(Http2Client.RESPONSE_BODY); + System.out.println("statusCode = " + statusCode); + System.out.println("headers = " + reference.get().getResponseHeaders()); + Assert.assertEquals(statusCode, 307); + // at this moment, an exception will help as it is redirected to localhost:8080 and it is not up. + } catch (Exception e) { + logger.error("IOException: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + + } + + /** + * In this test, we pass in the correct form parameters to with the right credential. The result should be + * a 302 redirect back to the client application with the authorization code as well as other parameters. + * This test is to simulate the scenario that the login form is filled in and the request is send back again. + * @throws Exception + */ @Test public void testAuthorizationCode() throws Exception { Map params = new HashMap<>(); params.put("j_username", "admin"); params.put("j_password", "123456"); params.put("response_type", "code"); + params.put("user_type", "employee"); params.put("client_id", "59f347a0-c92d-11e6-9d9d-cec0c932ce01"); + params.put("redirect_uri", "http://localhost:8080/authorization"); final AtomicReference reference = new AtomicReference<>(); final Http2Client client = Http2Client.getInstance(); @@ -56,7 +126,7 @@ public void testAuthorizationCode() throws Exception { public void run() { final ClientRequest request = new ClientRequest().setMethod(Methods.POST).setPath("/oauth2/code"); request.getRequestHeaders().put(Headers.HOST, "localhost"); - request.getRequestHeaders().put(Headers.CONTENT_TYPE, "application/json"); + request.getRequestHeaders().put(Headers.CONTENT_TYPE, FormEncodedDataDefinition.APPLICATION_X_WWW_FORM_URLENCODED); request.getRequestHeaders().put(Headers.TRANSFER_ENCODING, "chunked"); connection.sendRequest(request, client.createClientCallback(reference, latch, s)); } @@ -65,9 +135,8 @@ public void run() { int statusCode = reference.get().getResponseCode(); String body = reference.get().getAttachment(Http2Client.RESPONSE_BODY); System.out.println("statusCode = " + statusCode); - System.out.println("body = " + body); - - //Assert.assertEquals(statusCode, 302); + System.out.println("headers = " + reference.get().getResponseHeaders()); + Assert.assertEquals(statusCode, 302); // at this moment, an exception will help as it is redirected to localhost:8080 and it is not up. } catch (Exception e) { logger.error("IOException: ", e); @@ -77,4 +146,5 @@ public void run() { } } + }