From 9187b5e90cfacef43a4a867c08a4f4d8c1ee0607 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Thu, 4 Apr 2024 10:54:05 -0700 Subject: [PATCH 1/3] feat(Microsoft Login): Register and Login Teacher (#263) --- .../dao/authentication/UserDetailsDao.java | 7 +- .../impl/HibernateUserDetailsDao.java | 30 +++-- .../impl/PersistentUserDetails.java | 16 ++- .../config/MicrosoftOpenIdConnectConfig.java | 51 +++++++ .../teacher/TeacherAPIController.java | 17 ++- .../controllers/user/UserAPIController.java | 4 + .../MicrosoftUserNotFoundException.java | 27 ++++ .../filters/GoogleOpenIdConnectFilter.java | 2 + ...MicrosoftAuthenticationFailureHandler.java | 30 +++++ .../filters/MicrosoftOpenIdConnectFilter.java | 124 ++++++++++++++++++ .../filters/NoopAuthenticationManager.java | 14 ++ .../authentication/UserDetailsService.java | 12 +- .../impl/UserDetailsServiceImpl.java | 6 +- .../portal/spring/impl/WebSecurityConfig.java | 38 ++++-- .../application-dockerdev-sample.properties | 10 ++ .../resources/application_sample.properties | 10 ++ src/main/resources/wise_db_init.sql | 1 + 17 files changed, 356 insertions(+), 43 deletions(-) create mode 100644 src/main/java/org/wise/portal/presentation/web/config/MicrosoftOpenIdConnectConfig.java create mode 100644 src/main/java/org/wise/portal/presentation/web/exception/MicrosoftUserNotFoundException.java create mode 100644 src/main/java/org/wise/portal/presentation/web/filters/MicrosoftAuthenticationFailureHandler.java create mode 100644 src/main/java/org/wise/portal/presentation/web/filters/MicrosoftOpenIdConnectFilter.java create mode 100644 src/main/java/org/wise/portal/presentation/web/filters/NoopAuthenticationManager.java diff --git a/src/main/java/org/wise/portal/dao/authentication/UserDetailsDao.java b/src/main/java/org/wise/portal/dao/authentication/UserDetailsDao.java index 6d73a22308..1e619206a6 100644 --- a/src/main/java/org/wise/portal/dao/authentication/UserDetailsDao.java +++ b/src/main/java/org/wise/portal/dao/authentication/UserDetailsDao.java @@ -22,7 +22,6 @@ import java.util.List; - import org.wise.portal.dao.SimpleDao; import org.wise.portal.domain.authentication.MutableUserDetails; @@ -32,8 +31,14 @@ public interface UserDetailsDao extends SimpleDao { boolean hasUsername(String username); + T retrieveByName(String name); + T retrieveByGoogleUserId(String googleUserId); + + T retrieveByMicrosoftUserId(String userId); + List retrieveAllStudentUsernames(); + List retrieveAllTeacherUsernames(); } diff --git a/src/main/java/org/wise/portal/dao/authentication/impl/HibernateUserDetailsDao.java b/src/main/java/org/wise/portal/dao/authentication/impl/HibernateUserDetailsDao.java index 2d89723691..61196c1e08 100644 --- a/src/main/java/org/wise/portal/dao/authentication/impl/HibernateUserDetailsDao.java +++ b/src/main/java/org/wise/portal/dao/authentication/impl/HibernateUserDetailsDao.java @@ -45,8 +45,8 @@ * @author Cynick Young */ @Repository -public class HibernateUserDetailsDao extends AbstractHibernateDao implements - UserDetailsDao { +public class HibernateUserDetailsDao extends AbstractHibernateDao + implements UserDetailsDao { @PersistenceContext private EntityManager entityManager; @@ -62,8 +62,8 @@ public PersistentUserDetails retrieveByName(String username) { CriteriaBuilder cb = getCriteriaBuilder(); CriteriaQuery cq = cb.createQuery(PersistentUserDetails.class); Root persistentUserDetailsRoot = cq.from(PersistentUserDetails.class); - cq.select(persistentUserDetailsRoot).where( - cb.equal(persistentUserDetailsRoot.get("username"), username)); + cq.select(persistentUserDetailsRoot) + .where(cb.equal(persistentUserDetailsRoot.get("username"), username)); TypedQuery query = entityManager.createQuery(cq); return query.getResultStream().findFirst().orElse(null); } @@ -73,8 +73,8 @@ public List retrieveAllTeacherUsernames() { CriteriaQuery cq = cb.createQuery(String.class); Root persistentUserDetailsRoot = cq.from(PersistentUserDetails.class); Root teacherUserDetailsRoot = cq.from(TeacherUserDetails.class); - cq.select(persistentUserDetailsRoot.get("username")).where( - cb.equal(persistentUserDetailsRoot.get("id"), teacherUserDetailsRoot.get("id"))); + cq.select(persistentUserDetailsRoot.get("username")) + .where(cb.equal(persistentUserDetailsRoot.get("id"), teacherUserDetailsRoot.get("id"))); TypedQuery query = entityManager.createQuery(cq); return query.getResultList(); } @@ -84,8 +84,8 @@ public List retrieveAllStudentUsernames() { CriteriaQuery cq = cb.createQuery(String.class); Root persistentUserDetailsRoot = cq.from(PersistentUserDetails.class); Root studentUserDetailsRoot = cq.from(StudentUserDetails.class); - cq.select(persistentUserDetailsRoot.get("username")).where( - cb.equal(persistentUserDetailsRoot.get("id"), studentUserDetailsRoot.get("id"))); + cq.select(persistentUserDetailsRoot.get("username")) + .where(cb.equal(persistentUserDetailsRoot.get("id"), studentUserDetailsRoot.get("id"))); TypedQuery query = entityManager.createQuery(cq); return query.getResultList(); } @@ -94,8 +94,18 @@ public PersistentUserDetails retrieveByGoogleUserId(String googleUserId) { CriteriaBuilder cb = getCriteriaBuilder(); CriteriaQuery cq = cb.createQuery(PersistentUserDetails.class); Root persistentUserDetailsRoot = cq.from(PersistentUserDetails.class); - cq.select(persistentUserDetailsRoot).where( - cb.equal(persistentUserDetailsRoot.get("googleUserId"), googleUserId)); + cq.select(persistentUserDetailsRoot) + .where(cb.equal(persistentUserDetailsRoot.get("googleUserId"), googleUserId)); + TypedQuery query = entityManager.createQuery(cq); + return query.getResultStream().findFirst().orElse(null); + } + + public PersistentUserDetails retrieveByMicrosoftUserId(String userId) { + CriteriaBuilder cb = getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(PersistentUserDetails.class); + Root persistentUserDetailsRoot = cq.from(PersistentUserDetails.class); + cq.select(persistentUserDetailsRoot) + .where(cb.equal(persistentUserDetailsRoot.get("microsoftUserId"), userId)); TypedQuery query = entityManager.createQuery(cq); return query.getResultStream().findFirst().orElse(null); } diff --git a/src/main/java/org/wise/portal/domain/authentication/impl/PersistentUserDetails.java b/src/main/java/org/wise/portal/domain/authentication/impl/PersistentUserDetails.java index 9b6fa9770f..3c972faba3 100644 --- a/src/main/java/org/wise/portal/domain/authentication/impl/PersistentUserDetails.java +++ b/src/main/java/org/wise/portal/domain/authentication/impl/PersistentUserDetails.java @@ -124,7 +124,8 @@ public class PersistentUserDetails implements MutableUserDetails { // However, Acegi Security deals with an array. There are internal methods // to convert to and from the different data structures. @ManyToMany(targetEntity = PersistentGrantedAuthority.class, fetch = FetchType.EAGER) - @JoinTable(name = PersistentUserDetails.GRANTED_AUTHORITY_JOIN_TABLE_NAME, joinColumns = { @JoinColumn(name = USER_DETAILS_JOIN_COLUMN_NAME, nullable = false) }, inverseJoinColumns = @JoinColumn(name = GRANTED_AUTHORITY_JOIN_COLUMN_NAME, nullable = false)) + @JoinTable(name = PersistentUserDetails.GRANTED_AUTHORITY_JOIN_TABLE_NAME, joinColumns = { + @JoinColumn(name = USER_DETAILS_JOIN_COLUMN_NAME, nullable = false) }, inverseJoinColumns = @JoinColumn(name = GRANTED_AUTHORITY_JOIN_COLUMN_NAME, nullable = false)) private Set grantedAuthorities = null; @Column(name = PersistentUserDetails.COLUMN_NAME_PASSWORD, nullable = false) @@ -181,6 +182,11 @@ public class PersistentUserDetails implements MutableUserDetails { @Setter private String googleUserId; + @Column(name = "microsoftUserId") + @Getter + @Setter + private String microsoftUserId; + @Column(name = PersistentUserDetails.COLUMN_NAME_RESET_PASSWORD_VERIFICATION_CODE_REQUEST_TIME, nullable = true) @Getter @Setter @@ -220,9 +226,8 @@ private Set getGrantedAuthorities() { } @SuppressWarnings("unused") - private synchronized void setGrantedAuthorities( - Set grantedAuthorities) { - /* Used only for persistence */ + private synchronized void setGrantedAuthorities(Set grantedAuthorities) { + /* Used only for persistence */ this.grantedAuthorities = grantedAuthorities; } @@ -266,8 +271,7 @@ private void setEnabled(Boolean enabled) { public int hashCode() { final int PRIME = 31; int result = 1; - result = PRIME * result - + ((this.username == null) ? 0 : this.username.hashCode()); + result = PRIME * result + ((this.username == null) ? 0 : this.username.hashCode()); return result; } diff --git a/src/main/java/org/wise/portal/presentation/web/config/MicrosoftOpenIdConnectConfig.java b/src/main/java/org/wise/portal/presentation/web/config/MicrosoftOpenIdConnectConfig.java new file mode 100644 index 0000000000..a4ba1b37f7 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/config/MicrosoftOpenIdConnectConfig.java @@ -0,0 +1,51 @@ +package org.wise.portal.presentation.web.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; + +import java.util.Arrays; + +@Configuration +@EnableOAuth2Client +public class MicrosoftOpenIdConnectConfig { + + @Value("${microsoft.accessTokenUri:}") + private String accessTokenUri; + + @Value("${microsoft.clientId:}") + private String clientId; + + @Value("${microsoft.clientSecret:}") + private String clientSecret; + + @Value("${microsoft.redirectUri:}") + private String redirectUri; + + @Value("${microsoft.userAuthorizationUri:}") + private String userAuthorizationUri; + + @Bean("microsoftOAuth2ProtectedResourceDetails") + public OAuth2ProtectedResourceDetails microsoftOpenId() { + final AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails(); + details.setClientId(clientId); + details.setClientSecret(clientSecret); + details.setAccessTokenUri(accessTokenUri); + details.setUserAuthorizationUri(userAuthorizationUri); + details.setScope(Arrays.asList("openid", "email", "profile")); + details.setPreEstablishedRedirectUri(redirectUri); + details.setUseCurrentUri(false); + return details; + } + + @Bean("microsoftOpenIdRestTemplate") + public OAuth2RestTemplate microsoftOpenIdRestTemplate(final OAuth2ClientContext clientContext) { + final OAuth2RestTemplate template = new OAuth2RestTemplate(microsoftOpenId(), clientContext); + return template; + } +} diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java index 14626c18ba..a8bf4acadc 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherAPIController.java @@ -157,9 +157,13 @@ ResponseEntity> createTeacherAccount( tud.setState(teacherFields.get("state")); tud.setCountry(teacherFields.get("country")); String googleUserId = teacherFields.get("googleUserId"); - if (isUsingGoogleUserId(googleUserId)) { + String microsoftUserId = teacherFields.get("microsoftUserId"); + if (isSet(googleUserId)) { tud.setGoogleUserId(googleUserId); tud.setPassword(RandomStringUtils.random(10, true, true)); + } else if (isSet(microsoftUserId)) { + tud.setMicrosoftUserId(microsoftUserId); + tud.setPassword(RandomStringUtils.random(10, true, true)); } else { String password = teacherFields.get("password"); if (!passwordService.isValid(password)) { @@ -180,14 +184,15 @@ ResponseEntity> createTeacherAccount( String username = createdUser.getUserDetails().getUsername(); String sendEmailEnabledStr = appProperties.getProperty("send_email_enabled", "false"); Boolean iSendEmailEnabled = Boolean.valueOf(sendEmailEnabledStr); + boolean socialAccount = this.isSet(googleUserId) || this.isSet(microsoftUserId); if (iSendEmailEnabled) { - sendCreateTeacherAccountEmail(email, displayName, username, googleUserId, locale, request); + sendCreateTeacherAccountEmail(email, displayName, username, socialAccount, locale, request); } return createRegisterSuccessResponse(username); } private void sendCreateTeacherAccountEmail(String email, String displayName, String username, - String googleUserId, Locale locale, HttpServletRequest request) { + boolean socialAccount, Locale locale, HttpServletRequest request) { String fromEmail = appProperties.getProperty("portalemailaddress"); String[] recipients = { email }; String defaultSubject = messageSource.getMessage( @@ -201,7 +206,7 @@ private void sendCreateTeacherAccountEmail(String email, String displayName, Str new Object[] { username }, Locale.US); String gettingStartedUrl = getGettingStartedUrl(request); String message; - if (isUsingGoogleUserId(googleUserId)) { + if (socialAccount) { message = messageSource.getMessage( "presentation.web.controllers.teacher.registerTeacherController.welcomeTeacherEmailBodyNoUsername", new Object[] { displayName, gettingStartedUrl }, defaultBody, locale); @@ -221,8 +226,8 @@ private String getGettingStartedUrl(HttpServletRequest request) { return ControllerUtil.getPortalUrlString(request) + "/help/getting-started"; } - private boolean isUsingGoogleUserId(String googleUserId) { - return googleUserId != null && !googleUserId.isEmpty(); + private boolean isSet(String value) { + return value != null && !value.isEmpty(); } private List> getRunSharedOwnersList(Run run) { diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java index f3937d2223..91ec223af0 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java @@ -80,6 +80,9 @@ public class UserAPIController { @Autowired protected MessageSource messageSource; + @Value("${microsoft.clientId:}") + protected String microsoftClientId = ""; + @Autowired protected StudentService studentService; @@ -163,6 +166,7 @@ protected HashMap getConfig(HttpServletRequest request) { config.put("googleClientId", googleClientId); config.put("isGoogleClassroomEnabled", isGoogleClassroomEnabled()); config.put("logOutURL", contextPath + "/api/logout"); + config.put("microsoftClientId", microsoftClientId); config.put("recaptchaPublicKey", appProperties.getProperty("recaptcha_public_key")); config.put("wiseHostname", appProperties.getProperty("wise.hostname")); config.put("wise4Hostname", appProperties.getProperty("wise4.hostname")); diff --git a/src/main/java/org/wise/portal/presentation/web/exception/MicrosoftUserNotFoundException.java b/src/main/java/org/wise/portal/presentation/web/exception/MicrosoftUserNotFoundException.java new file mode 100644 index 0000000000..ab1fb8b593 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/exception/MicrosoftUserNotFoundException.java @@ -0,0 +1,27 @@ +package org.wise.portal.presentation.web.exception; + +import java.util.Map; + +import org.springframework.security.core.AuthenticationException; + +public class MicrosoftUserNotFoundException extends AuthenticationException { + private static final long serialVersionUID = 1L; + private Map authInfo; + + public MicrosoftUserNotFoundException(String msg, Map authInfo) { + super(msg); + this.authInfo = authInfo; + } + + public String getEmail() { + return authInfo.get("email"); + } + + public String getMicrosoftId() { + return authInfo.get("sub"); + } + + public String getName() { + return authInfo.get("name"); + } +} diff --git a/src/main/java/org/wise/portal/presentation/web/filters/GoogleOpenIdConnectFilter.java b/src/main/java/org/wise/portal/presentation/web/filters/GoogleOpenIdConnectFilter.java index 987b626ff2..72b5a0dabc 100644 --- a/src/main/java/org/wise/portal/presentation/web/filters/GoogleOpenIdConnectFilter.java +++ b/src/main/java/org/wise/portal/presentation/web/filters/GoogleOpenIdConnectFilter.java @@ -40,6 +40,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; @@ -70,6 +71,7 @@ public class GoogleOpenIdConnectFilter extends AbstractAuthenticationProcessingF private String googleJwkUrl; @Autowired + @Qualifier("googleOpenIdRestTemplate") private OAuth2RestTemplate googleOpenIdRestTemplate; @Autowired diff --git a/src/main/java/org/wise/portal/presentation/web/filters/MicrosoftAuthenticationFailureHandler.java b/src/main/java/org/wise/portal/presentation/web/filters/MicrosoftAuthenticationFailureHandler.java new file mode 100644 index 0000000000..3617708df9 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/filters/MicrosoftAuthenticationFailureHandler.java @@ -0,0 +1,30 @@ +package org.wise.portal.presentation.web.filters; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.transaction.annotation.Transactional; +import org.wise.portal.presentation.web.exception.MicrosoftUserNotFoundException; + +public class MicrosoftAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Override + @Transactional + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + if (request.getAttribute("redirectUrl").toString().contains("join")) { + String microsoftId = ((MicrosoftUserNotFoundException) exception).getMicrosoftId(); + String name = ((MicrosoftUserNotFoundException) exception).getName(); + String email = ((MicrosoftUserNotFoundException) exception).getEmail(); + response.sendRedirect(request.getAttribute("redirectUrl").toString() + ";mID=" + microsoftId + + ";name=" + name + ";email=" + email); + } else { + response.sendRedirect("/join?microsoftUserNotFound=true"); + } + } +} diff --git a/src/main/java/org/wise/portal/presentation/web/filters/MicrosoftOpenIdConnectFilter.java b/src/main/java/org/wise/portal/presentation/web/filters/MicrosoftOpenIdConnectFilter.java new file mode 100644 index 0000000000..61a198d8bf --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/filters/MicrosoftOpenIdConnectFilter.java @@ -0,0 +1,124 @@ +package org.wise.portal.presentation.web.filters; + +import java.io.IOException; +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.util.Date; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.jwt.Jwt; +import org.springframework.security.jwt.JwtHelper; +import org.springframework.security.jwt.crypto.sign.RsaVerifier; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.wise.portal.presentation.web.exception.MicrosoftUserNotFoundException; +import org.wise.portal.service.authentication.UserDetailsService; + +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.UrlJwkProvider; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class MicrosoftOpenIdConnectFilter extends AbstractAuthenticationProcessingFilter { + + @Value("${microsoft.clientId:}") + private String microsoftClientId; + + @Value("${microsoft.issuer:}") + private String microsoftIssuer; + + @Value("${microsoft.jwkUrl:}") + private String microsoftJwkUrl; + + @Autowired + @Qualifier("microsoftOpenIdRestTemplate") + private OAuth2RestTemplate microsoftOpenIdRestTemplate; + + @Autowired + private UserDetailsService userDetailsService; + + public MicrosoftOpenIdConnectFilter(String defaultFilterProcessesUrl) { + super(defaultFilterProcessesUrl); + setAuthenticationManager(new NoopAuthenticationManager()); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, + HttpServletResponse response) throws AuthenticationException, IOException, ServletException { + saveRequestParameter(request, "redirectUrl"); + OAuth2AccessToken accessToken; + try { + accessToken = microsoftOpenIdRestTemplate.getAccessToken(); + } catch (final OAuth2Exception e) { + throw new BadCredentialsException("Could not obtain access token", e); + } + final String idToken = accessToken.getAdditionalInformation().get("id_token").toString(); + String kid = JwtHelper.headers(idToken).get("kid"); + Jwt tokenDecoded = null; + try { + tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier(kid)); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + final Map authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), + Map.class); + verifyClaims(authInfo); + String microsoftUserId = authInfo.get("sub"); + final UserDetails user = userDetailsService.loadUserByMicrosoftUserId(microsoftUserId); + invalidateAccessToken(); + if (user != null) { + if (request.getAttribute("redirectUrl").toString().contains("join")) { + response.sendRedirect("/join/microsoftUserAlreadyExists"); + return null; + } else { + return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); + } + } else { + throw new MicrosoftUserNotFoundException("Microsoft user not found", authInfo); + } + } + + private void saveRequestParameter(HttpServletRequest request, String parameterName) { + String parameterValue = request.getParameter(parameterName); + String parameterFromState = (String) microsoftOpenIdRestTemplate.getOAuth2ClientContext() + .removePreservedState(parameterName); + microsoftOpenIdRestTemplate.getOAuth2ClientContext().setPreservedState(parameterName, + parameterValue); + request.setAttribute(parameterName, parameterFromState); + } + + private void verifyClaims(Map claims) { + int exp = (int) claims.get("exp"); + Date expireDate = new Date(exp * 1000L); + Date now = new Date(); + if (expireDate.before(now) || !claims.get("iss").equals(microsoftIssuer) + || !claims.get("aud").equals(microsoftClientId)) { + throw new RuntimeException("Invalid claims"); + } + } + + private RsaVerifier verifier(String kid) throws Exception { + JwkProvider provider = new UrlJwkProvider(new URL(microsoftJwkUrl)); + Jwk jwk = provider.get(kid); + return new RsaVerifier((RSAPublicKey) jwk.getPublicKey()); + } + + private void invalidateAccessToken() { + microsoftOpenIdRestTemplate.getOAuth2ClientContext().setAccessToken((OAuth2AccessToken) null); + } +} diff --git a/src/main/java/org/wise/portal/presentation/web/filters/NoopAuthenticationManager.java b/src/main/java/org/wise/portal/presentation/web/filters/NoopAuthenticationManager.java new file mode 100644 index 0000000000..c036dcaa2a --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/filters/NoopAuthenticationManager.java @@ -0,0 +1,14 @@ +package org.wise.portal.presentation.web.filters; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +public class NoopAuthenticationManager implements AuthenticationManager { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + throw new UnsupportedOperationException( + "No authentication should be done with this AuthenticationManager"); + } +} diff --git a/src/main/java/org/wise/portal/service/authentication/UserDetailsService.java b/src/main/java/org/wise/portal/service/authentication/UserDetailsService.java index 26dd69f94c..488f5141e1 100644 --- a/src/main/java/org/wise/portal/service/authentication/UserDetailsService.java +++ b/src/main/java/org/wise/portal/service/authentication/UserDetailsService.java @@ -31,8 +31,8 @@ * Provides WISE-specific ROLES on top of what is already available in WISE * @author Hiroki Terashima */ -public interface UserDetailsService extends - org.springframework.security.core.userdetails.UserDetailsService { +public interface UserDetailsService + extends org.springframework.security.core.userdetails.UserDetailsService { String ANONYMOUS_ROLE = "ROLE_ANONYMOUS"; @@ -72,7 +72,7 @@ public interface UserDetailsService extends * @throws DuplicateAuthorityException if authority is not unique. */ MutableGrantedAuthority createGrantedAuthority(MutableGrantedAuthority mutableGrantedAuthority) - throws DuplicateAuthorityException; + throws DuplicateAuthorityException; /** * Given an authority string, loads an authority from the data store. @@ -81,8 +81,7 @@ MutableGrantedAuthority createGrantedAuthority(MutableGrantedAuthority mutableGr * @return A MutableGrantedAuthority object * @throws AuthorityNotFoundException if authority is not in data store. */ - GrantedAuthority loadAuthorityByName(String authority) - throws AuthorityNotFoundException; + GrantedAuthority loadAuthorityByName(String authority) throws AuthorityNotFoundException; /** * Returns a list of all existing authorities in the system. @@ -94,8 +93,11 @@ GrantedAuthority loadAuthorityByName(String authority) UserDetails loadUserByGoogleUserId(String googleUserId); + UserDetails loadUserByMicrosoftUserId(String userId); + void updateStatsOnSuccessfulLogin(MutableUserDetails userDetails); List retrieveAllTeacherUsernames(); + List retrieveAllStudentUsernames(); } diff --git a/src/main/java/org/wise/portal/service/authentication/impl/UserDetailsServiceImpl.java b/src/main/java/org/wise/portal/service/authentication/impl/UserDetailsServiceImpl.java index 76d4dd8df1..b5b639e83f 100644 --- a/src/main/java/org/wise/portal/service/authentication/impl/UserDetailsServiceImpl.java +++ b/src/main/java/org/wise/portal/service/authentication/impl/UserDetailsServiceImpl.java @@ -64,6 +64,10 @@ public UserDetails loadUserByGoogleUserId(String googleUserId) { return this.userDetailsDao.retrieveByGoogleUserId(googleUserId); } + public UserDetails loadUserByMicrosoftUserId(String userId) { + return this.userDetailsDao.retrieveByMicrosoftUserId(userId); + } + @Override public void updateStatsOnSuccessfulLogin(MutableUserDetails userDetails) { ((MutableUserDetails) userDetails).incrementNumberOfLogins(); @@ -105,7 +109,7 @@ public GrantedAuthority loadAuthorityByName(String authority) throws AuthorityNo @Transactional(readOnly = true) public List retrieveAllAuthorities() { - return grantedAuthorityDao.getList(); + return grantedAuthorityDao.getList(); } @Transactional diff --git a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java index 0cee4aa7f1..b608d1272e 100644 --- a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java +++ b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java @@ -57,6 +57,8 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.session.Session; import org.wise.portal.presentation.web.filters.GoogleOpenIdConnectFilter; +import org.wise.portal.presentation.web.filters.MicrosoftAuthenticationFailureHandler; +import org.wise.portal.presentation.web.filters.MicrosoftOpenIdConnectFilter; import org.wise.portal.presentation.web.filters.WISEAuthenticationFailureHandler; import org.wise.portal.presentation.web.filters.WISEAuthenticationProcessingFilter; import org.wise.portal.presentation.web.filters.WISEAuthenticationSuccessHandler; @@ -77,23 +79,20 @@ public class WebSecurityConfig extends WebSecurityConfigurerA @Override protected void configure(HttpSecurity http) throws Exception { - http - .csrf().disable() + http.csrf().disable() .addFilterAfter(openSessionInViewFilter(), SecurityContextHolderAwareRequestFilter.class) .addFilterAfter(oAuth2ClientContextFilter(), OpenSessionInViewFilter.class) .addFilterAfter(googleOpenIdConnectFilter(), OAuth2ClientContextFilter.class) + .addFilterAfter(microsoftOpenIdConnectFilter(), OAuth2ClientContextFilter.class) .addFilterAfter(authenticationProcessingFilter(), GoogleOpenIdConnectFilter.class) - .authorizeRequests() - .antMatchers("/api/login/impersonate").hasAnyRole("ADMINISTRATOR","RESEARCHER") - .antMatchers("/admin/**").hasAnyRole("ADMINISTRATOR","RESEARCHER") - .antMatchers("/author/**").hasAnyRole("TEACHER") + .authorizeRequests().antMatchers("/api/login/impersonate") + .hasAnyRole("ADMINISTRATOR", "RESEARCHER").antMatchers("/admin/**") + .hasAnyRole("ADMINISTRATOR", "RESEARCHER").antMatchers("/author/**").hasAnyRole("TEACHER") .antMatchers("/project/notifyAuthor*/**").hasAnyRole("TEACHER") - .antMatchers("/student/account/info").hasAnyRole("TEACHER") - .antMatchers("/student/**").hasAnyRole("STUDENT") - .antMatchers("/studentStatus").hasAnyRole("TEACHER","STUDENT") - .antMatchers("/teacher/**").hasAnyRole("TEACHER") - .antMatchers("/sso/discourse").hasAnyRole("TEACHER","STUDENT") - .antMatchers("/").permitAll(); + .antMatchers("/student/account/info").hasAnyRole("TEACHER").antMatchers("/student/**") + .hasAnyRole("STUDENT").antMatchers("/studentStatus").hasAnyRole("TEACHER", "STUDENT") + .antMatchers("/teacher/**").hasAnyRole("TEACHER").antMatchers("/sso/discourse") + .hasAnyRole("TEACHER", "STUDENT").antMatchers("/").permitAll(); http.formLogin().loginPage("/login").permitAll(); http.logout().addLogoutHandler(wiseLogoutHandler()) .logoutRequestMatcher(new AntPathRequestMatcher("/api/logout")); @@ -113,7 +112,6 @@ public WISEAuthenticationProcessingFilter authenticationProcessingFilter() { return filter; } - @Bean public GoogleOpenIdConnectFilter googleOpenIdConnectFilter() { GoogleOpenIdConnectFilter filter = new GoogleOpenIdConnectFilter("/api/google-login"); @@ -122,6 +120,13 @@ public GoogleOpenIdConnectFilter googleOpenIdConnectFilter() { return filter; } + @Bean + public MicrosoftOpenIdConnectFilter microsoftOpenIdConnectFilter() { + MicrosoftOpenIdConnectFilter filter = new MicrosoftOpenIdConnectFilter("/api/microsoft-login"); + filter.setAuthenticationSuccessHandler(authSuccessHandler()); + filter.setAuthenticationFailureHandler(microsoftAuthFailureHandler()); + return filter; + } @Bean public OpenSessionInViewFilter openSessionInViewFilter() { @@ -155,7 +160,7 @@ public RoleVoter roleVoter() { @Bean public LogoutFilter logoutFilter() { - LogoutHandler[] handlers = new LogoutHandler[]{ new SecurityContextLogoutHandler() }; + LogoutHandler[] handlers = new LogoutHandler[] { new SecurityContextLogoutHandler() }; return new LogoutFilter("/", handlers); } @@ -183,6 +188,11 @@ public AuthenticationFailureHandler authFailureHandler() { return handler; } + @Bean + public AuthenticationFailureHandler microsoftAuthFailureHandler() { + return new MicrosoftAuthenticationFailureHandler(); + } + @Bean public ConsensusBased urlAccessDecisionManager() { List> decisionVoters = new ArrayList<>(); diff --git a/src/main/resources/application-dockerdev-sample.properties b/src/main/resources/application-dockerdev-sample.properties index 6854d6b72f..8aee770881 100644 --- a/src/main/resources/application-dockerdev-sample.properties +++ b/src/main/resources/application-dockerdev-sample.properties @@ -185,6 +185,16 @@ google.issuer=accounts.google.com google.jwkUrl=https://www.googleapis.com/oauth2/v2/certs google.tokens.dir= +### Microsoft Open Id (log in with Microsoft) ### + +microsoft.accessTokenUri= +microsoft.clientId= +microsoft.clientSecret= +microsoft.issuer= +microsoft.jwkUrl= +microsoft.userAuthorizationUri= +microsoft.redirectUri= + ### Discourse Single Sign-On ### # discourse_url=URL to your Discourse # discourse_sso_secret_key=agreed-upon secret key between your Discourse and WISE instance diff --git a/src/main/resources/application_sample.properties b/src/main/resources/application_sample.properties index a76b9546da..8dbeb4a8ec 100644 --- a/src/main/resources/application_sample.properties +++ b/src/main/resources/application_sample.properties @@ -185,6 +185,16 @@ google.issuer=accounts.google.com google.jwkUrl=https://www.googleapis.com/oauth2/v2/certs google.tokens.dir= +### Microsoft Open Id (log in with Microsoft) ### + +microsoft.accessTokenUri= +microsoft.clientId= +microsoft.clientSecret= +microsoft.issuer= +microsoft.jwkUrl= +microsoft.userAuthorizationUri= +microsoft.redirectUri= + ### Discourse Single Sign-On ### # discourse_url=URL to your Discourse # discourse_sso_secret_key=agreed-upon secret key between your Discourse and WISE instance diff --git a/src/main/resources/wise_db_init.sql b/src/main/resources/wise_db_init.sql index b141cec755..58354b5538 100644 --- a/src/main/resources/wise_db_init.sql +++ b/src/main/resources/wise_db_init.sql @@ -497,6 +497,7 @@ create table user_details ( email_address varchar(255), enabled bit not null, googleUserId varchar(255) null, + microsoftUserId varchar(255) null, reset_password_verification_code_request_time datetime null, reset_password_verification_code varchar(255) null, recent_failed_verification_code_attempt_time datetime null, From 53e6e5c15a74f37e347250e91e547ac9c66cbb4c Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Fri, 5 Apr 2024 11:58:27 -0700 Subject: [PATCH 2/3] Register and login student with Microsoft account (#265) --- .../web/controllers/student/StudentAPIController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/student/StudentAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/student/StudentAPIController.java index 2c4a034783..b8fd565ad6 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/student/StudentAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/student/StudentAPIController.java @@ -322,6 +322,9 @@ ResponseEntity> createStudentAccount( if (studentFields.containsKey("googleUserId")) { sud.setGoogleUserId(studentFields.get("googleUserId")); sud.setPassword(RandomStringUtils.random(10, true, true)); + } else if (studentFields.containsKey("microsoftUserId")) { + sud.setMicrosoftUserId(studentFields.get("microsoftUserId")); + sud.setPassword(RandomStringUtils.random(10, true, true)); } else { String password = studentFields.get("password"); if (!passwordService.isValid(password)) { From d346c01944ef3656e4401d108651002e381296cf Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Tue, 9 Apr 2024 08:52:00 -0700 Subject: [PATCH 3/3] Refactor Google/Microsoft OpenIdConnectFilter. Extract common code to Abstract class (#267) --- .../filters/AbstractOpenIdConnectFilter.java | 88 +++++++++++++++ .../filters/GoogleOpenIdConnectFilter.java | 104 +++++------------- .../filters/MicrosoftOpenIdConnectFilter.java | 76 +++---------- 3 files changed, 130 insertions(+), 138 deletions(-) create mode 100644 src/main/java/org/wise/portal/presentation/web/filters/AbstractOpenIdConnectFilter.java diff --git a/src/main/java/org/wise/portal/presentation/web/filters/AbstractOpenIdConnectFilter.java b/src/main/java/org/wise/portal/presentation/web/filters/AbstractOpenIdConnectFilter.java new file mode 100644 index 0000000000..a0e4e01b2e --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/filters/AbstractOpenIdConnectFilter.java @@ -0,0 +1,88 @@ +package org.wise.portal.presentation.web.filters; + +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.util.Date; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.jwt.crypto.sign.RsaVerifier; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.wise.portal.service.authentication.UserDetailsService; + +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.UrlJwkProvider; + +public abstract class AbstractOpenIdConnectFilter extends AbstractAuthenticationProcessingFilter { + + protected String clientId; + protected String issuer; + protected String jwkUrl; + protected OAuth2RestTemplate openIdRestTemplate; + + @Autowired + protected UserDetailsService userDetailsService; + + protected AbstractOpenIdConnectFilter(String defaultFilterProcessesUrl) { + super(defaultFilterProcessesUrl); + setAuthenticationManager(new NoopAuthenticationManager()); + } + + protected OAuth2AccessToken getAccessToken() { + OAuth2AccessToken accessToken; + try { + accessToken = openIdRestTemplate.getAccessToken(); + } catch (final OAuth2Exception e) { + throw new BadCredentialsException("Could not obtain access token", e); + } + return accessToken; + } + + protected void saveRequestParams(HttpServletRequest request) { + saveRequestParameter(request, "accessCode"); + saveRequestParameter(request, "redirectUrl"); + } + + protected void saveRequestParameter(HttpServletRequest request, String parameterName) { + String parameterValue = request.getParameter(parameterName); + String parameterFromState = (String) openIdRestTemplate.getOAuth2ClientContext() + .removePreservedState(parameterName); + openIdRestTemplate.getOAuth2ClientContext().setPreservedState(parameterName, parameterValue); + request.setAttribute(parameterName, parameterFromState); + } + + protected void verifyClaims(Map claims) { + int exp = (int) claims.get("exp"); + Date expireDate = new Date(exp * 1000L); + Date now = new Date(); + if (expireDate.before(now) || !claims.get("iss").equals(issuer) + || !claims.get("aud").equals(clientId)) { + throw new RuntimeException("Invalid claims"); + } + } + + protected RsaVerifier verifier(String kid) throws Exception { + JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl)); + Jwk jwk = provider.get(kid); + return new RsaVerifier((RSAPublicKey) jwk.getPublicKey()); + } + + protected void invalidateAccessToken() { + openIdRestTemplate.getOAuth2ClientContext().setAccessToken((OAuth2AccessToken) null); + } + + protected abstract void setClientId(String clientId); + + protected abstract void setIssuer(String issuer); + + protected abstract void setJwkUrl(String jwkUrl); + + protected abstract void setOpenIdRestTemplate(OAuth2RestTemplate template); +} diff --git a/src/main/java/org/wise/portal/presentation/web/filters/GoogleOpenIdConnectFilter.java b/src/main/java/org/wise/portal/presentation/web/filters/GoogleOpenIdConnectFilter.java index 72b5a0dabc..96ceb84459 100644 --- a/src/main/java/org/wise/portal/presentation/web/filters/GoogleOpenIdConnectFilter.java +++ b/src/main/java/org/wise/portal/presentation/web/filters/GoogleOpenIdConnectFilter.java @@ -24,9 +24,6 @@ package org.wise.portal.presentation.web.filters; import java.io.IOException; -import java.net.URL; -import java.security.interfaces.RSAPublicKey; -import java.util.Date; import java.util.Map; import javax.servlet.FilterChain; @@ -34,15 +31,11 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import com.auth0.jwk.Jwk; -import com.auth0.jwk.JwkProvider; -import com.auth0.jwk.UrlJwkProvider; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -50,52 +43,25 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.jwt.Jwt; import org.springframework.security.jwt.JwtHelper; -import org.springframework.security.jwt.crypto.sign.RsaVerifier; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; -import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.wise.portal.domain.authentication.MutableUserDetails; -import org.wise.portal.service.authentication.UserDetailsService; import org.wise.portal.service.session.SessionService; -public class GoogleOpenIdConnectFilter extends AbstractAuthenticationProcessingFilter { - - @Value("${google.clientId:}") - private String googleClientId; - - @Value("${google.issuer:}") - private String googleIssuer; - - @Value("${google.jwkUrl:}") - private String googleJwkUrl; - - @Autowired - @Qualifier("googleOpenIdRestTemplate") - private OAuth2RestTemplate googleOpenIdRestTemplate; - - @Autowired - private UserDetailsService userDetailsService; +public class GoogleOpenIdConnectFilter extends AbstractOpenIdConnectFilter { @Autowired protected SessionService sessionService; public GoogleOpenIdConnectFilter(String defaultFilterProcessesUrl) { super(defaultFilterProcessesUrl); - setAuthenticationManager(new NoopAuthenticationManager()); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException { - saveRequestParameter(request, "accessCode"); - saveRequestParameter(request, "redirectUrl"); - OAuth2AccessToken accessToken; - try { - accessToken = googleOpenIdRestTemplate.getAccessToken(); - } catch (final OAuth2Exception e) { - throw new BadCredentialsException("Could not obtain access token", e); - } + saveRequestParams(request); + OAuth2AccessToken accessToken = getAccessToken(); try { final String idToken = accessToken.getAdditionalInformation().get("id_token").toString(); String kid = JwtHelper.headers(idToken).get("kid"); @@ -103,8 +69,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, final Map authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class); verifyClaims(authInfo); - String googleUserId = authInfo.get("sub"); - final UserDetails user = userDetailsService.loadUserByGoogleUserId(googleUserId); + final UserDetails user = userDetailsService.loadUserByGoogleUserId(authInfo.get("sub")); invalidateAccesToken(); if (user != null) { return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); @@ -116,47 +81,8 @@ public Authentication attemptAuthentication(HttpServletRequest request, } } - private void saveRequestParameter(HttpServletRequest request, String parameterName) { - String parameterValue = request.getParameter(parameterName); - String parameterFromState = (String) googleOpenIdRestTemplate.getOAuth2ClientContext() - .removePreservedState(parameterName); - googleOpenIdRestTemplate.getOAuth2ClientContext().setPreservedState(parameterName, - parameterValue); - request.setAttribute(parameterName, parameterFromState); - } - private void invalidateAccesToken() { - googleOpenIdRestTemplate.getOAuth2ClientContext().setAccessToken((OAuth2AccessToken) null); - } - - public void verifyClaims(Map claims) { - int exp = (int) claims.get("exp"); - Date expireDate = new Date(exp * 1000L); - Date now = new Date(); - if (expireDate.before(now) || !claims.get("iss").equals(googleIssuer) - || !claims.get("aud").equals(googleClientId)) { - throw new RuntimeException("Invalid claims"); - } - } - - private RsaVerifier verifier(String kid) throws Exception { - JwkProvider provider = new UrlJwkProvider(new URL(googleJwkUrl)); - Jwk jwk = provider.get(kid); - return new RsaVerifier((RSAPublicKey) jwk.getPublicKey()); - } - - public void setRestTemplate(OAuth2RestTemplate restTemplate2) { - googleOpenIdRestTemplate = restTemplate2; - } - - private static class NoopAuthenticationManager implements AuthenticationManager { - - @Override - public Authentication authenticate(Authentication authentication) - throws AuthenticationException { - throw new UnsupportedOperationException( - "No authentication should be done with this AuthenticationManager"); - } + openIdRestTemplate.getOAuth2ClientContext().setAccessToken((OAuth2AccessToken) null); } @Override @@ -168,4 +94,24 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR super.successfulAuthentication(request, response, chain, authentication); } + @Value("${google.clientId:}") + protected void setClientId(String clientId) { + this.clientId = clientId; + } + + @Value("${google.issuer:}") + protected void setIssuer(String issuer) { + this.issuer = issuer; + } + + @Value("${google.jwkUrl:}") + protected void setJwkUrl(String jwkUrl) { + this.jwkUrl = jwkUrl; + } + + @Autowired + @Qualifier("googleOpenIdRestTemplate") + protected void setOpenIdRestTemplate(OAuth2RestTemplate template) { + this.openIdRestTemplate = template; + } } diff --git a/src/main/java/org/wise/portal/presentation/web/filters/MicrosoftOpenIdConnectFilter.java b/src/main/java/org/wise/portal/presentation/web/filters/MicrosoftOpenIdConnectFilter.java index 61a198d8bf..28f549dd67 100644 --- a/src/main/java/org/wise/portal/presentation/web/filters/MicrosoftOpenIdConnectFilter.java +++ b/src/main/java/org/wise/portal/presentation/web/filters/MicrosoftOpenIdConnectFilter.java @@ -1,9 +1,6 @@ package org.wise.portal.presentation.web.filters; import java.io.IOException; -import java.net.URL; -import java.security.interfaces.RSAPublicKey; -import java.util.Date; import java.util.Map; import javax.servlet.ServletException; @@ -13,59 +10,29 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.jwt.Jwt; import org.springframework.security.jwt.JwtHelper; -import org.springframework.security.jwt.crypto.sign.RsaVerifier; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; -import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.wise.portal.presentation.web.exception.MicrosoftUserNotFoundException; -import org.wise.portal.service.authentication.UserDetailsService; -import com.auth0.jwk.Jwk; -import com.auth0.jwk.JwkProvider; -import com.auth0.jwk.UrlJwkProvider; import com.fasterxml.jackson.databind.ObjectMapper; -public class MicrosoftOpenIdConnectFilter extends AbstractAuthenticationProcessingFilter { - - @Value("${microsoft.clientId:}") - private String microsoftClientId; - - @Value("${microsoft.issuer:}") - private String microsoftIssuer; - - @Value("${microsoft.jwkUrl:}") - private String microsoftJwkUrl; - - @Autowired - @Qualifier("microsoftOpenIdRestTemplate") - private OAuth2RestTemplate microsoftOpenIdRestTemplate; - - @Autowired - private UserDetailsService userDetailsService; +public class MicrosoftOpenIdConnectFilter extends AbstractOpenIdConnectFilter { public MicrosoftOpenIdConnectFilter(String defaultFilterProcessesUrl) { super(defaultFilterProcessesUrl); - setAuthenticationManager(new NoopAuthenticationManager()); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { - saveRequestParameter(request, "redirectUrl"); - OAuth2AccessToken accessToken; - try { - accessToken = microsoftOpenIdRestTemplate.getAccessToken(); - } catch (final OAuth2Exception e) { - throw new BadCredentialsException("Could not obtain access token", e); - } + saveRequestParams(request); + OAuth2AccessToken accessToken = getAccessToken(); final String idToken = accessToken.getAdditionalInformation().get("id_token").toString(); String kid = JwtHelper.headers(idToken).get("kid"); Jwt tokenDecoded = null; @@ -78,8 +45,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, final Map authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class); verifyClaims(authInfo); - String microsoftUserId = authInfo.get("sub"); - final UserDetails user = userDetailsService.loadUserByMicrosoftUserId(microsoftUserId); + final UserDetails user = userDetailsService.loadUserByMicrosoftUserId(authInfo.get("sub")); invalidateAccessToken(); if (user != null) { if (request.getAttribute("redirectUrl").toString().contains("join")) { @@ -93,32 +59,24 @@ public Authentication attemptAuthentication(HttpServletRequest request, } } - private void saveRequestParameter(HttpServletRequest request, String parameterName) { - String parameterValue = request.getParameter(parameterName); - String parameterFromState = (String) microsoftOpenIdRestTemplate.getOAuth2ClientContext() - .removePreservedState(parameterName); - microsoftOpenIdRestTemplate.getOAuth2ClientContext().setPreservedState(parameterName, - parameterValue); - request.setAttribute(parameterName, parameterFromState); + @Value("${microsoft.clientId:}") + protected void setClientId(String clientId) { + this.clientId = clientId; } - private void verifyClaims(Map claims) { - int exp = (int) claims.get("exp"); - Date expireDate = new Date(exp * 1000L); - Date now = new Date(); - if (expireDate.before(now) || !claims.get("iss").equals(microsoftIssuer) - || !claims.get("aud").equals(microsoftClientId)) { - throw new RuntimeException("Invalid claims"); - } + @Value("${microsoft.issuer:}") + protected void setIssuer(String issuer) { + this.issuer = issuer; } - private RsaVerifier verifier(String kid) throws Exception { - JwkProvider provider = new UrlJwkProvider(new URL(microsoftJwkUrl)); - Jwk jwk = provider.get(kid); - return new RsaVerifier((RSAPublicKey) jwk.getPublicKey()); + @Value("${microsoft.jwkUrl:}") + protected void setJwkUrl(String jwkUrl) { + this.jwkUrl = jwkUrl; } - private void invalidateAccessToken() { - microsoftOpenIdRestTemplate.getOAuth2ClientContext().setAccessToken((OAuth2AccessToken) null); + @Autowired + @Qualifier("microsoftOpenIdRestTemplate") + protected void setOpenIdRestTemplate(OAuth2RestTemplate template) { + this.openIdRestTemplate = template; } }