Skip to content

Commit

Permalink
Use OpenID Connect end_session_endpoint when available. Better usage …
Browse files Browse the repository at this point in the history
…of the signout page. Fixed error page.
  • Loading branch information
ymarcon committed Sep 16, 2022
1 parent 5e604cc commit fb1510e
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 49 deletions.
4 changes: 4 additions & 0 deletions agate-core/src/main/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ server:
error:
whitelabel:
enabled: false
servlet:
session:
cookie:
name: JSESSIONID_8081

metrics:
jmx.enabled: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
Expand All @@ -70,6 +70,8 @@ public class OAuthResource {

private final List<String> AGATE_SCOPES = Lists.newArrayList("openid", "email", "profile");

private final String APPLICATION_ATTRIBUTE = "application";

@Inject
private ConfigurationService configurationService;

Expand Down Expand Up @@ -105,6 +107,7 @@ public Response validateAuthorize(@Context HttpServletRequest servletRequest) th
redirectURI = validateClientApplication(clientId, oAuthRequest.getParam(OAuth.OAUTH_REDIRECT_URI));
// check user has access to the application
authorizationValidator.validateApplication(servletRequest, user, clientId);
SecurityUtils.getSubject().getSession().setAttribute(APPLICATION_ATTRIBUTE, clientId);
validateScope(oAuthRequest.getScopes());
OAuthRequestData data = new OAuthRequestData(clientId, redirectURI, oAuthRequest);
Authorization authorization = authorizationService.find(user.getName(), data.getClientId());
Expand Down Expand Up @@ -205,11 +208,14 @@ public Response access(@Context HttpServletRequest servletRequest, MultivaluedMa
@GET
@Path("/logout")
public Response logout(@QueryParam("post_logout_redirect_uri") String postLogoutRedirectUri) {
// redirect to the sign-out page
// Redirect to the sign-out page
try {
String signoutPagePath = "../../../signout" + (Strings.isNullOrEmpty(postLogoutRedirectUri) ? "" : "?post_logout_redirect_uri=" + postLogoutRedirectUri);
return Response.temporaryRedirect(new URI(signoutPagePath)).build();
} catch (URISyntaxException e) {
UriBuilder builder = UriBuilder.fromUri(configurationService.getPublicUrl() + "/signout");
if (!Strings.isNullOrEmpty(postLogoutRedirectUri)) {
builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri);
}
return Response.temporaryRedirect(builder.build()).build();
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,28 @@
package org.obiba.agate.web.controller;

import com.google.common.base.Strings;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.json.JSONException;
import org.json.JSONObject;
import org.obiba.agate.config.ClientConfiguration;
import org.obiba.agate.domain.*;
import org.obiba.agate.domain.Application;
import org.obiba.agate.domain.RealmUsage;
import org.obiba.agate.domain.User;
import org.obiba.agate.security.OidcAuthConfigurationProvider;
import org.obiba.agate.service.ApplicationService;
import org.obiba.agate.service.ConfigurationService;
import org.obiba.agate.service.NoSuchUserException;
import org.obiba.agate.service.UserService;
import org.obiba.agate.web.controller.domain.AuthConfiguration;
import org.obiba.agate.web.controller.domain.OidcProvider;
import org.obiba.agate.web.support.URLUtils;
import org.obiba.oidc.OIDCConfiguration;
import org.obiba.oidc.utils.OIDCHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -31,15 +41,16 @@

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.Collection;
import java.util.Locale;
import java.util.stream.Collectors;

import static org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE_REQUEST_ATTRIBUTE_NAME;

@Controller
public class SignController {

private static final Logger log = LoggerFactory.getLogger(SignController.class);

@Inject
private ConfigurationService configurationService;

Expand All @@ -49,9 +60,15 @@ public class SignController {
@Inject
private OidcAuthConfigurationProvider oidcAuthConfigurationProvider;

@Inject
private ApplicationService applicationService;

@Inject
private UserService userService;

@Value("${logout.confirm:false}")
private boolean confirmLogout;

@GetMapping("/signin")
public ModelAndView signin(HttpServletRequest request,
@CookieValue(value = "NG_TRANSLATE_LANG_KEY", required = false, defaultValue = "en") String locale,
Expand Down Expand Up @@ -113,20 +130,62 @@ public ModelAndView signupWith(@CookieValue(value = "u_auth", required = false,
public ModelAndView signout(@RequestParam(value = "post_logout_redirect_uri", required = false) String postLogoutRedirectUri) {
Subject subject = SecurityUtils.getSubject();
if (!subject.isAuthenticated()) {
if (!Strings.isNullOrEmpty(postLogoutRedirectUri)) {
return new ModelAndView("redirect:" + postLogoutRedirectUri);
ModelAndView mv = new ModelAndView("signout");
mv.getModel().put("authenticated", false);
mv.getModel().put("confirm", false);
mv.getModel().put("postLogoutRedirectUri", postLogoutRedirectUri);
return mv;
}

// Validate redirect uri if there is an associated application
Object appAttr = SecurityUtils.getSubject().getSession().getAttribute("application");
String appName = appAttr == null ? null : appAttr.toString();
if (!Strings.isNullOrEmpty(appName)) {
Application application = applicationService.findByIdOrName(appName);
if (application == null || (!Strings.isNullOrEmpty(postLogoutRedirectUri) && application.getRedirectURIs().stream().noneMatch(postLogoutRedirectUri::startsWith))) {
ModelAndView mv = new ModelAndView("redirect:error");
mv.getModel().put("error", "400");
mv.getModel().put("message", "invalid-redirect");
return mv;
}
// for consistency
return new ModelAndView("redirect:signin?redirect=" + configurationService.getContextPath() + "/signout");
}

String newPostLogoutRedirectUri = postLogoutRedirectUri;
try {
ModelAndView mv = new ModelAndView("signout");
mv.getModel().put("postLogoutRedirectUri", postLogoutRedirectUri);
return mv;
} catch (Exception e) {
return new ModelAndView("redirect:" + configurationService.getContextPath() + "/");
User user = userService.getCurrentUser();
OIDCConfiguration oidcConfig = oidcAuthConfigurationProvider.getConfiguration(user.getRealm());
if (oidcConfig != null) {
try {
OIDCProviderMetadata metadata = OIDCHelper.discoverProviderMetaData(oidcConfig);
URI logoutEndpoint = metadata.getEndSessionEndpointURI();
if (logoutEndpoint != null) {
// tell OIDC provider to logout and redirect to the agate's signout page with its own redirection...
log.debug("Using {} OIDC logout endpoint: {}", user.getRealm(), logoutEndpoint);
String logoutRedirect = String.format("%s/signout", configurationService.getPublicUrl());
if (!Strings.isNullOrEmpty(postLogoutRedirectUri)) {
logoutRedirect = UriBuilder.fromUri(logoutRedirect)
.queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
.build().toString();
}
UriBuilder oidcLogoutURIBuilder = UriBuilder.fromUri(logoutEndpoint);
if (!Strings.isNullOrEmpty(logoutRedirect)) {
oidcLogoutURIBuilder.queryParam("post_logout_redirect_uri", logoutRedirect);
}
newPostLogoutRedirectUri = oidcLogoutURIBuilder.build().toString();
}
} catch (Exception e) {
log.error("Error when getting OIDC logout URL {}", user.getRealm(), e);
}
}
} catch (NoSuchUserException e) {
// ignore
}

ModelAndView mv = new ModelAndView("signout");
mv.getModel().put("authenticated", true);
mv.getModel().put("confirm", confirmLogout);
mv.getModel().put("postLogoutRedirectUri", newPostLogoutRedirectUri);
return mv;
}

@GetMapping("/just-registered")
Expand Down
8 changes: 4 additions & 4 deletions agate-webapp/src/main/resources/_templates/error.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@
<div class="error-content">
<h3><i class="fas fa-exclamation-triangle text-warning"></i>
<#if msg??>
${msg}
<@message msg/>
<#else >
"Not found"
<@message "not-found"/>
</#if>
</h3>

<p>
We could not access the page you were looking for.
Meanwhile, you may <a href="${contextPath}/">return to home</a>.
<@message "error-text"/>
</p>
<a href="${contextPath}/" class="btn btn-outline-info"><i class="fa fa-home"></i> <@message "error-back-home"/></a>
</div>
<!-- /.error-content -->
</div><!-- /.container-fluid -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</#if>
</li>
<li class="nav-item">
<a class="btn btn-outline-danger" href="#" onclick="agatejs.signout();"><@message "sign-out"/></a>
<a class="btn btn-outline-danger" href="${contextPath}/signout"><@message "sign-out"/></a>
</li>
<#elseif config??>
<#if config.locales?size != 1>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
<#if !authenticated>
agatejs.redirect('${postLogoutRedirectUri!".."}');
<#elseif !confirm>
agatejs.signout('${postLogoutRedirectUri!".."}');
</#if>
</script>
42 changes: 18 additions & 24 deletions agate-webapp/src/main/resources/_templates/signout.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,29 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body id="confirm-page" class="hold-transition login-page">
<div class="login-box">
<div class="login-logo">
<a href=".."><b>${config.name!""}</b></a>
</div>
<!-- /.login-logo -->
<div class="card">
<div class="card-body login-card-body">
<p class="login-box-msg"><@message "confirm-sign-out"/></p>
<div>
<a class="btn btn-outline-danger" href="#" onclick="agatejs.signout('${postLogoutRedirectUri!""}');"><@message "sign-out"/></a>
<a class="btn btn-outline-info ml-2" href="${postLogoutRedirectUri!"/"}"><@message "keep-signed-in"/></a>

<#if confirm>
<div class="login-box">
<div class="login-logo">
<a href=".."><b>${config.name!""}</b></a>
</div>
<!-- /.login-logo -->
<div class="card">
<div class="card-body login-card-body">
<p class="login-box-msg"><@message "confirm-sign-out"/></p>
<div>
<a class="btn btn-outline-danger" href="#" onclick="agatejs.signout('${postLogoutRedirectUri!""}');"><@message "sign-out"/></a>
<a class="btn btn-outline-info ml-2" href="${postLogoutRedirectUri!"/"}"><@message "keep-signed-in"/></a>
</div>
</div>
<!-- /.login-card-body -->
</div>
<!-- /.login-card-body -->
</div>
</div>
<!-- /.login-box -->
<!-- /.login-box -->
</#if>

<#include "libs/scripts.ftl">

<script>
agatejs.confirmAndSetPassword("#form", function (errorKey) {
var alertId = "#alert" + errorKey;
$(alertId).removeClass("d-none");
setTimeout(function() {
$(alertId).addClass("d-none");
}, 5000);
});
</script>
<#include "libs/signout-scripts.ftl">

</body>
</html>
4 changes: 4 additions & 0 deletions agate-webapp/src/main/resources/i18n/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ submit = Submit
alert = Alert
security-info = Security information
2fa-info = You can enhance your account security with two-factor authentication.
not-found = Not found
error-text = We could not access the page you were looking for.
error-back-home = Return home

#
# Sign in, up, out, reset password
Expand Down Expand Up @@ -80,6 +83,7 @@ password-too-short = Password is too short (minimum is 8 characters).
password-no-match = The passwords do not match.
2fa-caption = Enter 6-digits PIN code
validate = Validate
invalid-redirect = Invalid redirection

#
# Profile
Expand Down
4 changes: 4 additions & 0 deletions agate-webapp/src/main/resources/i18n/messages_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ submit = Soumettre
alert = Alerte
security-info = Information de sécurité
2fa-info = Vous pouvez améliorer la sécurité de votre compte en activant l'authentification à deux facteurs.
not-found = Non trouvée
error-text = Nous ne pouvons accèder à la page demandée.
error-back-home = Retour à l'accueil

#
# Sign in, up, out, reset password
Expand Down Expand Up @@ -80,6 +83,7 @@ password-too-short = Le mot de passe est trop court (minimum de 8 caractères).
password-no-match = Les mots de passe ne correspondent pas.
2fa-caption = Entrer le code NIP à 6 chiffres
validate = Valider
invalid-redirect = Redirection non valide

#
# Profile
Expand Down
13 changes: 11 additions & 2 deletions agate-webapp/src/main/webapp/assets/js/agate.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,15 +335,23 @@ var agatejs = (function() {
});
};

const agateRedirect = function(redirectUrl) {
if (redirectUrl && redirectUrl.startsWith('http')) {
window.location = redirectUrl;
} else {
var redirect = normalizeUrl('/');
$.redirect(redirect, {}, 'GET');
}
}

const agateSignout = function(redirectUrl) {
const removeAgateSession = function() {
$.ajax({
type: 'DELETE',
url: normalizeUrl('/ws/auth/session/_current')
})
.always(function() {
var redirect = redirectUrl ? redirectUrl : normalizeUrl('/');
$.redirect(redirect, {}, 'GET');
agateRedirect(redirectUrl);
});
};

Expand Down Expand Up @@ -390,6 +398,7 @@ var agatejs = (function() {
'normalizeUrl': normalizeUrl,
'signin': agateSignin,
'signout': agateSignout,
'redirect': agateRedirect,
'signup': agateSignup,
'forgotPassword': agateForgotPassword,
'resetPassword': agateResetPassword,
Expand Down

0 comments on commit fb1510e

Please sign in to comment.