Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login with Microsoft #272

Merged
merged 3 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

import java.util.List;


import org.wise.portal.dao.SimpleDao;
import org.wise.portal.domain.authentication.MutableUserDetails;

Expand All @@ -32,8 +31,14 @@
public interface UserDetailsDao<T extends MutableUserDetails> extends SimpleDao<T> {

boolean hasUsername(String username);

T retrieveByName(String name);

T retrieveByGoogleUserId(String googleUserId);

T retrieveByMicrosoftUserId(String userId);

List<String> retrieveAllStudentUsernames();

List<String> retrieveAllTeacherUsernames();
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
* @author Cynick Young
*/
@Repository
public class HibernateUserDetailsDao extends AbstractHibernateDao<MutableUserDetails> implements
UserDetailsDao<MutableUserDetails> {
public class HibernateUserDetailsDao extends AbstractHibernateDao<MutableUserDetails>
implements UserDetailsDao<MutableUserDetails> {

@PersistenceContext
private EntityManager entityManager;
Expand All @@ -62,8 +62,8 @@ public PersistentUserDetails retrieveByName(String username) {
CriteriaBuilder cb = getCriteriaBuilder();
CriteriaQuery<PersistentUserDetails> cq = cb.createQuery(PersistentUserDetails.class);
Root<PersistentUserDetails> 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<PersistentUserDetails> query = entityManager.createQuery(cq);
return query.getResultStream().findFirst().orElse(null);
}
Expand All @@ -73,8 +73,8 @@ public List<String> retrieveAllTeacherUsernames() {
CriteriaQuery<String> cq = cb.createQuery(String.class);
Root<PersistentUserDetails> persistentUserDetailsRoot = cq.from(PersistentUserDetails.class);
Root<TeacherUserDetails> 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<String> query = entityManager.createQuery(cq);
return query.getResultList();
}
Expand All @@ -84,8 +84,8 @@ public List<String> retrieveAllStudentUsernames() {
CriteriaQuery<String> cq = cb.createQuery(String.class);
Root<PersistentUserDetails> persistentUserDetailsRoot = cq.from(PersistentUserDetails.class);
Root<StudentUserDetails> 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<String> query = entityManager.createQuery(cq);
return query.getResultList();
}
Expand All @@ -94,8 +94,18 @@ public PersistentUserDetails retrieveByGoogleUserId(String googleUserId) {
CriteriaBuilder cb = getCriteriaBuilder();
CriteriaQuery<PersistentUserDetails> cq = cb.createQuery(PersistentUserDetails.class);
Root<PersistentUserDetails> 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<PersistentUserDetails> query = entityManager.createQuery(cq);
return query.getResultStream().findFirst().orElse(null);
}

public PersistentUserDetails retrieveByMicrosoftUserId(String userId) {
CriteriaBuilder cb = getCriteriaBuilder();
CriteriaQuery<PersistentUserDetails> cq = cb.createQuery(PersistentUserDetails.class);
Root<PersistentUserDetails> persistentUserDetailsRoot = cq.from(PersistentUserDetails.class);
cq.select(persistentUserDetailsRoot)
.where(cb.equal(persistentUserDetailsRoot.get("microsoftUserId"), userId));
TypedQuery<PersistentUserDetails> query = entityManager.createQuery(cq);
return query.getResultStream().findFirst().orElse(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<GrantedAuthority> grantedAuthorities = null;

@Column(name = PersistentUserDetails.COLUMN_NAME_PASSWORD, nullable = false)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -220,9 +226,8 @@ private Set<GrantedAuthority> getGrantedAuthorities() {
}

@SuppressWarnings("unused")
private synchronized void setGrantedAuthorities(
Set<GrantedAuthority> grantedAuthorities) {
/* Used only for persistence */
private synchronized void setGrantedAuthorities(Set<GrantedAuthority> grantedAuthorities) {
/* Used only for persistence */
this.grantedAuthorities = grantedAuthorities;
}

Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,9 @@ ResponseEntity<Map<String, Object>> 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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,13 @@ ResponseEntity<Map<String, Object>> 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)) {
Expand All @@ -180,14 +184,15 @@ ResponseEntity<Map<String, Object>> 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(
Expand All @@ -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);
Expand All @@ -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<HashMap<String, Object>> getRunSharedOwnersList(Run run) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ public class UserAPIController {
@Autowired
protected MessageSource messageSource;

@Value("${microsoft.clientId:}")
protected String microsoftClientId = "";

@Autowired
protected StudentService studentService;

Expand Down Expand Up @@ -163,6 +166,7 @@ protected HashMap<String, Object> 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"));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> authInfo;

public MicrosoftUserNotFoundException(String msg, Map<String, String> 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");
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading