Reworked OpenId demo into test using a local test OpenIdProvider

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2019-09-11 12:14:47 +10:00
parent c67ac736df
commit 85cdc0d6c4
5 changed files with 386 additions and 140 deletions

View File

@ -54,5 +54,11 @@
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -158,8 +158,9 @@ public class OpenIdCredentials implements Serializable
if (sections.length != 3)
throw new IllegalArgumentException("JWT does not contain 3 sections");
String jwtHeaderString = new String(Base64.getDecoder().decode(sections[0]), StandardCharsets.UTF_8);
String jwtClaimString = new String(Base64.getDecoder().decode(sections[1]), StandardCharsets.UTF_8);
Base64.Decoder decoder = Base64.getDecoder();
String jwtHeaderString = new String(decoder.decode(sections[0]), StandardCharsets.UTF_8);
String jwtClaimString = new String(decoder.decode(sections[1]), StandardCharsets.UTF_8);
String jwtSignature = sections[2];
Map<String, Object> jwtHeader = (Map)JSON.parse(jwtHeaderString);

View File

@ -18,123 +18,53 @@
package org.eclipse.jetty.security.openid;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.Principal;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.security.Constraint;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class OpenIdAuthenticationDemo
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class OpenIdAuthenticationTest
{
public static class AdminPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.getWriter().println("<p>this is the admin page "+request.getUserPrincipal()+": <a href=\"/\">Home</a></p>");
}
}
public static final String CLIENT_ID = "testClient101";
public static final String CLIENT_SECRET = "secret37989798";
public static class LoginPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.getWriter().println("<p>you logged in <a href=\"/\">Home</a></p>");
}
}
private OpenIdProvider openIdProvider;
private Server server;
private ServerConnector connector;
private HttpClient client;
public static class LogoutPage extends HttpServlet
@BeforeEach
public void setup() throws Exception
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
request.getSession().invalidate();
response.sendRedirect("/");
}
}
openIdProvider = new OpenIdProvider(CLIENT_ID, CLIENT_SECRET);
openIdProvider.start();
public static class HomePage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
response.getWriter().println("<h1>Home Page</h1>");
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null)
{
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println("<p>Welcome: " + userInfo.get("name") + "</p>");
response.getWriter().println("<a href=\"/profile\">Profile</a><br>");
response.getWriter().println("<a href=\"/admin\">Admin</a><br>");
response.getWriter().println("<a href=\"/logout\">Logout</a><br>");
}
else
{
response.getWriter().println("<p>Please Login <a href=\"/login\">Login</a></p>");
}
}
}
public static class ProfilePage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println("<!-- Add icon library -->\n" +
"<div class=\"card\">\n" +
" <img src=\""+userInfo.get("picture")+"\" style=\"width:30%\">\n" +
" <h1>"+ userInfo.get("name") +"</h1>\n" +
" <p class=\"title\">"+userInfo.get("email")+"</p>\n" +
" <p>UserId: " + userInfo.get("sub") +"</p>\n" +
"</div>");
response.getWriter().println("<a href=\"/\">Home</a><br>");
response.getWriter().println("<a href=\"/logout\">Logout</a><br>");
}
}
public static class ErrorPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
response.getWriter().println("<h1>error: not authorized</h1>");
response.getWriter().println("<p>" + request.getUserPrincipal() + "</p>");
}
}
public static void main(String[] args) throws Exception
{
Server server = new Server(8080);
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
// Add servlets
context.addServlet(ProfilePage.class, "/profile");
context.addServlet(LoginPage.class, "/login");
context.addServlet(AdminPage.class, "/admin");
context.addServlet(LogoutPage.class, "/logout");
context.addServlet(HomePage.class, "/*");
context.addServlet(ErrorPage.class, "/error");
@ -168,47 +98,8 @@ public class OpenIdAuthenticationDemo
securityHandler.addConstraintMapping(loginMapping);
securityHandler.addConstraintMapping(adminMapping);
/**/
// Google Authentication
OpenIdConfiguration configuration = new OpenIdConfiguration(
"https://accounts.google.com/",
"1051168419525-5nl60mkugb77p9j194mrh287p1e0ahfi.apps.googleusercontent.com",
"XT_MIsSv_aUCGollauCaJY8S");
configuration.addScopes("email", "profile");
/**/
/*
// Microsoft Authentication
OpenIdConfiguration configuration = new OpenIdConfiguration(
"https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0",
"5f05dea8-2bd9-45de-b30f-cf5c102b8784",
"IfhQJKi-5[vxhh_=ldqt0y4PkV3z_1ca");
*/
/*
// Yahoo Authentication
OpenIdConfiguration configuration = new OpenIdConfiguration(
"https://login.yahoo.com",
"dj0yJmk9ME5Id05yTkdGNDdPJmQ9WVdrOU9VcHVZWEp4TkdrbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmc3Y9MCZ4PTE2",
"1e7f0eeb0ba0af9d9198f9be760f66ae3ea9e3b5");
configuration.addScopes("sdps-r");
*/
// Create a realm.properties file to associate roles with users
Path tmpDir = Paths.get(System.getProperty("java.io.tmpdir"));
Path tmpPath = Files.createTempFile(tmpDir, "realm", ".properties");
tmpPath.toFile().deleteOnExit();
try (BufferedWriter writer = Files.newBufferedWriter(tmpPath, StandardCharsets.UTF_8, StandardOpenOption.WRITE))
{
// <userId>:[,<rolename> ...]
writer.write("114260987481616800581:,admin");
}
// This must be added to the OpenIdLoginService in constructor below
HashLoginService hashLoginService = new HashLoginService();
hashLoginService.setConfig(tmpPath.toAbsolutePath().toString());
hashLoginService.setHotReload(true);
// Authentication using local OIDC Provider
OpenIdConfiguration configuration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET);
// Configure OpenIdLoginService optionally providing a base LoginService to provide user roles
OpenIdLoginService loginService = new OpenIdLoginService(configuration);//, hashLoginService);
@ -219,6 +110,117 @@ public class OpenIdAuthenticationDemo
context.setSecurityHandler(securityHandler);
server.start();
server.join();
String redirectUri = "http://localhost:"+connector.getLocalPort() + "/j_security_check";
openIdProvider.addRedirectUri(redirectUri);
client = new HttpClient();
client.start();
}
@AfterEach
public void stop() throws Exception
{
openIdProvider.stop();
server.stop();
}
@Test
public void testLoginLogout() throws Exception
{
String appUriString = "http://localhost:"+connector.getLocalPort();
// Initially not authenticated
ContentResponse response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String[] content = response.getContentAsString().split("\n");
assertThat(content.length, is(1));
assertThat(content[0], is("not authenticated"));
// Request to login is success
response = client.GET(appUriString + "/login");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("\n");
assertThat(content.length, is(1));
assertThat(content[0], is("success"));
// Now authenticated we can get info
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("\n");
assertThat(content.length, is(3));
assertThat(content[0], is("userId: 123456789"));
assertThat(content[1], is("name: FirstName LastName"));
assertThat(content[2], is("email: FirstName@fake-email.com"));
// Request to admin page gives 403 as we do not have admin role
response = client.GET(appUriString + "/admin");
assertThat(response.getStatus(), is(HttpStatus.FORBIDDEN_403));
// We are no longer authenticated after logging out
response = client.GET(appUriString + "/logout");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("\n");
assertThat(content.length, is(1));
assertThat(content[0], is("not authenticated"));
}
public static class LoginPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.getWriter().println("success");
}
}
public static class LogoutPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
request.getSession().invalidate();
response.sendRedirect("/");
}
}
public static class AdminPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println(userInfo.get("sub") + ": success");
}
}
public static class HomePage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType("text/plain");
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null)
{
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println("userId: " + userInfo.get("sub"));
response.getWriter().println("name: " + userInfo.get("name"));
response.getWriter().println("email: " + userInfo.get("email"));
}
else
{
response.getWriter().println("not authenticated");
}
}
}
public static class ErrorPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType("text/plain");
response.getWriter().println("not authorized");
}
}
}

View File

@ -0,0 +1,236 @@
package org.eclipse.jetty.security.openid;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
public class OpenIdProvider extends ContainerLifeCycle
{
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
private static final String AUTH_PATH = "/auth";
private static final String TOKEN_PATH = "/token";
private final Map<String, User> issuedAuthCodes = new HashMap<>();
protected final String clientId;
protected final String clientSecret;
protected final List<String> redirectUris = new ArrayList<>();
private String provider;
private Server server;
private ServerConnector connector;
public OpenIdProvider(String clientId, String clientSecret)
{
this.clientId = clientId;
this.clientSecret = clientSecret;
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
ServletContextHandler contextHandler = new ServletContextHandler();
contextHandler.setContextPath("/");
contextHandler.addServlet(new ServletHolder(new OpenIdConfigServlet()), CONFIG_PATH);
contextHandler.addServlet(new ServletHolder(new OpenIdAuthEndpoint()), AUTH_PATH);
contextHandler.addServlet(new ServletHolder(new OpenIdTokenEndpoint()), TOKEN_PATH);
server.setHandler(contextHandler);
addBean(server);
}
@Override
protected void doStart() throws Exception
{
super.doStart();
provider = "http://localhost:" + connector.getLocalPort();
}
public String getProvider()
{
if (!isStarted())
throw new IllegalStateException();
return provider;
}
public void addRedirectUri(String uri)
{
redirectUris.add(uri);
}
public class OpenIdAuthEndpoint extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
if (!clientId.equals(req.getParameter("client_id")))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id");
return;
}
String redirectUri = req.getParameter("redirect_uri");
if (!redirectUris.contains(redirectUri))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
return;
}
String scopeString = req.getParameter("scope");
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
if (!scopes.contains("openid"))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
return;
}
if (!"code".equals(req.getParameter("response_type")))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code");
return;
}
String state = req.getParameter("state");
if (state == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
return;
}
String authCode = UUID.randomUUID().toString().replace("-", "");
User user = new User(123456789, "FirstName", "LastName");
issuedAuthCodes.put(authCode, user);
final Request baseRequest = Request.getBaseRequest(req);
final Response baseResponse = baseRequest.getResponse();
redirectUri += "?code=" + authCode + "&state=" + state;
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ?
HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, resp.encodeRedirectURL(redirectUri));
}
}
public class OpenIdTokenEndpoint extends HttpServlet
{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
String code = req.getParameter("code");
if (!clientId.equals(req.getParameter("client_id")) ||
!clientSecret.equals(req.getParameter("client_secret")) ||
!redirectUris.contains(req.getParameter("redirect_uri")) ||
!"authorization_code".equals(req.getParameter("grant_type")) ||
code == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request");
return;
}
User user = issuedAuthCodes.remove(code);
if (user == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code");
return;
}
String jwtHeader = "{\"INFO\": \"this is not used or checked in our implementation\"}";
String jwtBody = user.getIdToken();
String jwtSignature = "we do not validate signature as we use the authorization code flow";
Base64.Encoder encoder = Base64.getEncoder();
String jwt = encoder.encodeToString(jwtHeader.getBytes()) + "." +
encoder.encodeToString(jwtBody.getBytes()) + "." +
encoder.encodeToString(jwtSignature.getBytes());
String accessToken = "ABCDEFG";
long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();
String response = "{" +
"\"access_token\": \"" + accessToken + "\"," +
"\"id_token\": \"" + jwt + "\"," +
"\"expires_in\": " + expiry + "," +
"\"token_type\": \"Bearer\"" +
"}";
resp.setContentType("text/plain");
resp.getWriter().print(response);
}
}
public class OpenIdConfigServlet extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
String discoveryDocument = "{" +
"\"issuer\": \"" + provider + "\"," +
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
"}";
resp.getWriter().write(discoveryDocument);
}
}
public class User
{
private long subject;
private String firstName;
private String lastName;
public User(String firstName, String lastName)
{
this(new Random().nextLong(), firstName, lastName);
}
public User(long subject, String firstName, String lastName)
{
this.subject = subject;
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName()
{
return firstName;
}
public String getLastName()
{
return lastName;
}
public String getIdToken()
{
return "{" +
"\"iss\": \"" + provider + "\"," +
"\"sub\": \"" + subject + "\"," +
"\"aud\": \"" + clientId + "\"," +
"\"exp\": " + System.currentTimeMillis() + Duration.ofMinutes(1).toMillis() + "," +
"\"name\": \"" + firstName + " " + lastName + "\"," +
"\"email\": \"" + firstName + "@fake-email.com" + "\"" +
"}";
}
}
}

View File

@ -1,2 +1,3 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
# org.eclipse.jetty.LEVEL=DEBUG
# org.eclipse.jetty.security.openid.LEVEL=DEBUG