This authenticator implements authentication using OpenId Connect on top of OAuth 2.0. + * + *
The authenticator redirects unauthenticated requests to the identity providers authorization endpoint + * which will eventually redirect back to the redirectUri with an authorization code which will be exchanged with + * the token_endpoint for an id_token. The request is then restored back to the original uri requested. + * {@link SessionAuthentication} is then used to wrap Authentication results so that they are associated with the session.
+ */ +public class OpenIdAuthenticator extends LoginAuthenticator +{ + private static final Logger LOG = Log.getLogger(OpenIdAuthenticator.class); + + public static final String __USER_CLAIMS = "org.eclipse.jetty.security.openid.user_claims"; + public static final String __RESPONSE_JSON = "org.eclipse.jetty.security.openid.response"; + public static final String __ERROR_PAGE = "org.eclipse.jetty.security.openid.error_page"; + public static final String __J_URI = "org.eclipse.jetty.security.openid.URI"; + public static final String __J_POST = "org.eclipse.jetty.security.openid.POST"; + public static final String __J_METHOD = "org.eclipse.jetty.security.openid.METHOD"; + public static final String __CSRF_TOKEN = "org.eclipse.jetty.security.openid.csrf_token"; + public static final String __J_SECURITY_CHECK = "/j_security_check"; + + private OpenIdConfiguration _configuration; + private String _errorPage; + private String _errorPath; + private boolean _alwaysSaveUri; + + public OpenIdAuthenticator() + { + } + + public OpenIdAuthenticator(OpenIdConfiguration configuration, String errorPage) + { + this._configuration = configuration; + if (errorPage != null) + setErrorPage(errorPage); + } + + @Override + public void setConfiguration(AuthConfiguration configuration) + { + super.setConfiguration(configuration); + + String error = configuration.getInitParameter(__ERROR_PAGE); + if (error != null) + setErrorPage(error); + + if (_configuration != null) + return; + + LoginService loginService = configuration.getLoginService(); + if (!(loginService instanceof OpenIdLoginService)) + throw new IllegalArgumentException("invalid LoginService"); + this._configuration = ((OpenIdLoginService)loginService).getConfiguration(); + } + + @Override + public String getAuthMethod() + { + return Constraint.__OPENID_AUTH; + } + + /** + * If true, uris that cause a redirect to a login page will always + * be remembered. If false, only the first uri that leads to a login + * page redirect is remembered. + * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=379909 + * + * @param alwaysSave true to always save the uri + */ + public void setAlwaysSaveUri(boolean alwaysSave) + { + _alwaysSaveUri = alwaysSave; + } + + public boolean getAlwaysSaveUri() + { + return _alwaysSaveUri; + } + + private void setErrorPage(String path) + { + if (path == null || path.trim().length() == 0) + { + _errorPath = null; + _errorPage = null; + } + else + { + if (!path.startsWith("/")) + { + LOG.warn("error-page must start with /"); + path = "/" + path; + } + _errorPage = path; + _errorPath = path; + + if (_errorPath.indexOf('?') > 0) + _errorPath = _errorPath.substring(0, _errorPath.indexOf('?')); + } + } + + @Override + public UserIdentity login(String username, Object credentials, ServletRequest request) + { + if (LOG.isDebugEnabled()) + LOG.debug("login {} {} {}", username, credentials, request); + + UserIdentity user = super.login(username, credentials, request); + if (user != null) + { + HttpSession session = ((HttpServletRequest)request).getSession(); + Authentication cached = new SessionAuthentication(getAuthMethod(), user, credentials); + session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached); + session.setAttribute(__USER_CLAIMS, ((OpenIdCredentials)credentials).getClaims()); + session.setAttribute(__RESPONSE_JSON, ((OpenIdCredentials)credentials).getResponse()); + } + return user; + } + + @Override + public void logout(ServletRequest request) + { + super.logout(request); + HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpSession session = httpRequest.getSession(false); + + if (session == null) + return; + + //clean up session + session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED); + session.removeAttribute(__USER_CLAIMS); + session.removeAttribute(__RESPONSE_JSON); + } + + @Override + public void prepareRequest(ServletRequest request) + { + //if this is a request resulting from a redirect after auth is complete + //(ie its from a redirect to the original request uri) then due to + //browser handling of 302 redirects, the method may not be the same as + //that of the original request. Replace the method and original post + //params (if it was a post). + // + //See Servlet Spec 3.1 sec 13.6.3 + HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpSession session = httpRequest.getSession(false); + if (session == null || session.getAttribute(SessionAuthentication.__J_AUTHENTICATED) == null) + return; //not authenticated yet + + String juri = (String)session.getAttribute(__J_URI); + if (juri == null || juri.length() == 0) + return; //no original uri saved + + String method = (String)session.getAttribute(__J_METHOD); + if (method == null || method.length() == 0) + return; //didn't save original request method + + StringBuffer buf = httpRequest.getRequestURL(); + if (httpRequest.getQueryString() != null) + buf.append("?").append(httpRequest.getQueryString()); + + if (!juri.equals(buf.toString())) + return; //this request is not for the same url as the original + + //restore the original request's method on this request + if (LOG.isDebugEnabled()) + LOG.debug("Restoring original method {} for {} with method {}", method, juri, httpRequest.getMethod()); + Request baseRequest = Request.getBaseRequest(request); + baseRequest.setMethod(method); + } + + @Override + public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException + { + final HttpServletRequest request = (HttpServletRequest)req; + final HttpServletResponse response = (HttpServletResponse)res; + final Request baseRequest = Request.getBaseRequest(request); + final Response baseResponse = baseRequest.getResponse(); + + String uri = request.getRequestURI(); + if (uri == null) + uri = URIUtil.SLASH; + + mandatory |= isJSecurityCheck(uri); + if (!mandatory) + return new DeferredAuthentication(this); + + if (isErrorPage(URIUtil.addPaths(request.getServletPath(), request.getPathInfo())) && !DeferredAuthentication.isDeferred(response)) + return new DeferredAuthentication(this); + + try + { + // Handle a request for authentication. + if (isJSecurityCheck(uri)) + { + String authCode = request.getParameter("code"); + if (authCode != null) + { + // Verify anti-forgery state token + String state = request.getParameter("state"); + String antiForgeryToken = (String)request.getSession().getAttribute(__CSRF_TOKEN); + if (antiForgeryToken == null || !antiForgeryToken.equals(state)) + { + LOG.warn("auth failed 403: invalid state parameter"); + if (response != null) + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return Authentication.SEND_FAILURE; + } + + // Attempt to login with the provided authCode + OpenIdCredentials credentials = new OpenIdCredentials(authCode, getRedirectUri(request), _configuration); + UserIdentity user = login(null, credentials, request); + HttpSession session = request.getSession(false); + if (user != null) + { + // Redirect to original request + String nuri; + synchronized (session) + { + nuri = (String)session.getAttribute(__J_URI); + + if (nuri == null || nuri.length() == 0) + { + nuri = request.getContextPath(); + if (nuri.length() == 0) + nuri = URIUtil.SLASH; + } + } + OpenIdAuthentication openIdAuth = new OpenIdAuthentication(getAuthMethod(), user); + LOG.debug("authenticated {}->{}", openIdAuth, nuri); + + response.setContentLength(0); + int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); + baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(nuri)); + return openIdAuth; + } + } + + // not authenticated + if (LOG.isDebugEnabled()) + LOG.debug("OpenId authentication FAILED"); + if (_errorPage == null) + { + if (LOG.isDebugEnabled()) + LOG.debug("auth failed 403"); + if (response != null) + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("auth failed {}", _errorPage); + int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); + baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), _errorPage))); + } + + return Authentication.SEND_FAILURE; + } + + // Look for cached authentication + HttpSession session = request.getSession(false); + Authentication authentication = session == null ? null : (Authentication)session.getAttribute(SessionAuthentication.__J_AUTHENTICATED); + if (authentication != null) + { + // Has authentication been revoked? + if (authentication instanceof Authentication.User && + _loginService != null && + !_loginService.validate(((Authentication.User)authentication).getUserIdentity())) + { + LOG.debug("auth revoked {}", authentication); + session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED); + } + else + { + synchronized (session) + { + String jUri = (String)session.getAttribute(__J_URI); + if (jUri != null) + { + //check if the request is for the same url as the original and restore + //params if it was a post + LOG.debug("auth retry {}->{}", authentication, jUri); + StringBuffer buf = request.getRequestURL(); + if (request.getQueryString() != null) + buf.append("?").append(request.getQueryString()); + + if (jUri.equals(buf.toString())) + { + MultiMapthis is the admin page "+request.getUserPrincipal()+": Home
"); + } + } + + public static class LoginPage extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.getWriter().println("you logged in Home
"); + } + } + + public static class LogoutPage extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + request.getSession().invalidate(); + response.sendRedirect("/"); + } + } + + 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("Welcome: " + userInfo.get("name") + "
"); + response.getWriter().println("ProfilePlease Login Login
"); + } + } + } + + public static class ProfilePage extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); + Map"+userInfo.get("email")+"
\n" + + "UserId: " + userInfo.get("sub") +"
\n" + + "" + request.getUserPrincipal() + "
"); + } + } + + public static void main(String[] args) throws Exception + { + Server server = new Server(8080); + 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"); + + // configure security constraints + Constraint constraint = new Constraint(); + constraint.setName(Constraint.__OPENID_AUTH); + constraint.setRoles(new String[]{"**"}); + constraint.setAuthenticate(true); + + Constraint adminConstraint = new Constraint(); + adminConstraint.setName(Constraint.__OPENID_AUTH); + adminConstraint.setRoles(new String[]{"admin"}); + adminConstraint.setAuthenticate(true); + + // constraint mappings + ConstraintMapping profileMapping = new ConstraintMapping(); + profileMapping.setConstraint(constraint); + profileMapping.setPathSpec("/profile"); + ConstraintMapping loginMapping = new ConstraintMapping(); + loginMapping.setConstraint(constraint); + loginMapping.setPathSpec("/login"); + ConstraintMapping adminMapping = new ConstraintMapping(); + adminMapping.setConstraint(adminConstraint); + adminMapping.setPathSpec("/admin"); + + // security handler + ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); + securityHandler.setRealmName("OpenID Connect Authentication"); + securityHandler.addConstraintMapping(profileMapping); + 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/common/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)) + { + //