From 38a9e4ca2a191ba1fa9f2eec1b05f3d742775eaa Mon Sep 17 00:00:00 2001
From: Daniel Gerhardt <code@dgerhardt.net>
Date: Wed, 30 Jan 2019 16:16:06 +0100
Subject: [PATCH] Add support for OpenID Connect authentication

Only OIDC providers that support configuration discovery are supported.
---
 .../de/thm/arsnova/config/SecurityConfig.java | 28 ++++++++++++++++++-
 .../arsnova/controller/LoginController.java   | 24 ++++++++++++++++
 .../java/de/thm/arsnova/entities/User.java    | 17 +++++++----
 .../de/thm/arsnova/services/UserService.java  |  6 ++--
 src/main/resources/arsnova.properties.example | 11 ++++++++
 src/test/resources/arsnova.properties.example | 11 ++++++++
 6 files changed, 88 insertions(+), 9 deletions(-)

diff --git a/src/main/java/de/thm/arsnova/config/SecurityConfig.java b/src/main/java/de/thm/arsnova/config/SecurityConfig.java
index b040e5efc..f871342c2 100644
--- a/src/main/java/de/thm/arsnova/config/SecurityConfig.java
+++ b/src/main/java/de/thm/arsnova/config/SecurityConfig.java
@@ -30,6 +30,7 @@ import org.pac4j.core.config.Config;
 import org.pac4j.oauth.client.FacebookClient;
 import org.pac4j.oauth.client.TwitterClient;
 import org.pac4j.oidc.client.GoogleOidcClient;
+import org.pac4j.oidc.client.OidcClient;
 import org.pac4j.oidc.config.OidcConfiguration;
 import org.pac4j.springframework.security.web.CallbackFilter;
 import org.slf4j.Logger;
@@ -89,6 +90,7 @@ import java.util.List;
 @Profile("!test")
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
 	private static final String OAUTH_CALLBACK_PATH_SUFFIX = "/auth/oauth_callback";
+	private static final String OIDC_DISCOVERY_PATH_SUFFIX = "/.well-known/openid-configuration";
 	private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
 
 	@Autowired
@@ -111,6 +113,11 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
 	@Value("${security.cas.enabled}") private boolean casEnabled;
 	@Value("${security.cas-server-url}") private String casUrl;
 
+	@Value("${security.oidc.enabled}") private boolean oidcEnabled;
+	@Value("${security.oidc.issuer}") private String oidcIssuer;
+	@Value("${security.oidc.client-id}") private String oidcClientId;
+	@Value("${security.oidc.secret}") private String oidcSecret;
+
 	@Value("${security.facebook.enabled}") private boolean facebookEnabled;
 	@Value("${security.facebook.key}") private String facebookKey;
 	@Value("${security.facebook.secret}") private String facebookSecret;
@@ -142,7 +149,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
 			http.addFilter(casLogoutFilter());
 		}
 
-		if (facebookEnabled || googleEnabled || twitterEnabled) {
+		if (oidcEnabled || facebookEnabled || googleEnabled || twitterEnabled) {
 			CallbackFilter callbackFilter = new CallbackFilter(oauthConfig());
 			callbackFilter.setSuffix(OAUTH_CALLBACK_PATH_SUFFIX);
 			callbackFilter.setDefaultUrl(rootUrl + apiPath + "/");
@@ -165,6 +172,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
 			providers.add("cas");
 			auth.authenticationProvider(casAuthenticationProvider());
 		}
+		if (oidcEnabled) {
+			providers.add("oidc");
+		}
 		if (googleEnabled) {
 			providers.add("google");
 		}
@@ -376,6 +386,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
 	@Bean
 	public Config oauthConfig() {
 		List<Client> clients = new ArrayList<>();
+		if (oidcEnabled) {
+			clients.add(oidcClient());
+		}
 		if (facebookEnabled) {
 			clients.add(facebookClient());
 		}
@@ -389,6 +402,19 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
 		return new Config(rootUrl + apiPath + OAUTH_CALLBACK_PATH_SUFFIX, clients);
 	}
 
+	@Bean
+	public OidcClient oidcClient() {
+		OidcConfiguration config = new OidcConfiguration();
+		config.setDiscoveryURI(oidcIssuer + OIDC_DISCOVERY_PATH_SUFFIX);
+		config.setClientId(oidcClientId);
+		config.setSecret(oidcSecret);
+		config.setScope("openid");
+		OidcClient client = new OidcClient(config);
+		client.setCallbackUrl(rootUrl + apiPath + OAUTH_CALLBACK_PATH_SUFFIX + "?client_name=OidcClient");
+
+		return client;
+	}
+
 	@Bean
 	public FacebookClient facebookClient() {
 		final FacebookClient client = new FacebookClient(facebookKey, facebookSecret);
diff --git a/src/main/java/de/thm/arsnova/controller/LoginController.java b/src/main/java/de/thm/arsnova/controller/LoginController.java
index a2c4f0902..8fcfb6d67 100644
--- a/src/main/java/de/thm/arsnova/controller/LoginController.java
+++ b/src/main/java/de/thm/arsnova/controller/LoginController.java
@@ -26,6 +26,7 @@ import org.pac4j.core.exception.HttpAction;
 import org.pac4j.oauth.client.FacebookClient;
 import org.pac4j.oauth.client.TwitterClient;
 import org.pac4j.oidc.client.GoogleOidcClient;
+import org.pac4j.oidc.client.OidcClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -106,6 +107,12 @@ public class LoginController extends AbstractController {
 	@Value("${security.cas.image:}") private String casImage;
 	@Value("${security.cas.order}") private int casOrder;
 
+	@Value("${security.oidc.enabled}") private boolean oidcEnabled;
+	@Value("${security.oidc.allowed-roles:speaker,student}") private String[] oidcRoles;
+	@Value("${security.oidc.title:OIDC}") private String oidcTitle;
+	@Value("${security.oidc.image:}") private String oidcImage;
+	@Value("${security.oidc.order}") private int oidcOrder;
+
 	@Value("${security.facebook.enabled}") private boolean facebookEnabled;
 	@Value("${security.facebook.allowed-roles:speaker,student}") private String[] facebookRoles;
 	@Value("${security.facebook.order}") private int facebookOrder;
@@ -124,6 +131,9 @@ public class LoginController extends AbstractController {
 	@Autowired(required = false)
 	private DaoAuthenticationProvider daoProvider;
 
+	@Autowired(required = false)
+	private OidcClient oidcClient;
+
 	@Autowired(required = false)
 	private TwitterClient twitterClient;
 
@@ -276,6 +286,9 @@ public class LoginController extends AbstractController {
 
 		if (casEnabled && "cas".equals(type)) {
 			casEntryPoint.commence(request, response, null);
+		} else if (oidcEnabled && "oidc".equals(type)) {
+			result = new RedirectView(
+					oidcClient.getRedirectAction(new J2EContext(request, response)).getLocation());
 		} else if (twitterEnabled && "twitter".equals(type)) {
 			result = new RedirectView(
 					twitterClient.getRedirectAction(new J2EContext(request, response)).getLocation());
@@ -379,6 +392,17 @@ public class LoginController extends AbstractController {
 			services.add(sdesc);
 		}
 
+		if (oidcEnabled) {
+			ServiceDescription sdesc = new ServiceDescription(
+					"oidc",
+					oidcTitle,
+					MessageFormat.format(dialogUrl, "oidc"),
+					oidcRoles
+			);
+			sdesc.setOrder(oidcOrder);
+			services.add(sdesc);
+		}
+
 		if (facebookEnabled) {
 			ServiceDescription sdesc = new ServiceDescription(
 				"facebook",
diff --git a/src/main/java/de/thm/arsnova/entities/User.java b/src/main/java/de/thm/arsnova/entities/User.java
index 26bb83317..6d51da093 100644
--- a/src/main/java/de/thm/arsnova/entities/User.java
+++ b/src/main/java/de/thm/arsnova/entities/User.java
@@ -21,6 +21,7 @@ import de.thm.arsnova.services.UserSessionService;
 import org.jasig.cas.client.authentication.AttributePrincipal;
 import org.pac4j.oauth.profile.facebook.FacebookProfile;
 import org.pac4j.oauth.profile.twitter.TwitterProfile;
+import org.pac4j.oidc.profile.OidcProfile;
 import org.pac4j.oidc.profile.google.GoogleOidcProfile;
 import org.springframework.security.authentication.AnonymousAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -36,6 +37,7 @@ public class User implements Serializable {
 	public static final String FACEBOOK = "facebook";
 	public static final String THM = "thm";
 	public static final String LDAP = "ldap";
+	public static final String OIDC = "oidc";
 	public static final String ARSNOVA = "arsnova";
 	public static final String ANONYMOUS = "anonymous";
 	public static final String GUEST = "guest";
@@ -48,12 +50,17 @@ public class User implements Serializable {
 	private UserSessionService.Role role;
 	private boolean isAdmin;
 
-	public User(GoogleOidcProfile profile) {
-		if (!profile.getEmailVerified()) {
-			throw new IllegalArgumentException("Email is not verified.");
+	public User(OidcProfile profile) {
+		if (profile instanceof GoogleOidcProfile) {
+			if (!profile.getEmailVerified()) {
+				throw new IllegalArgumentException("Email is not verified.");
+			}
+			setUsername(profile.getEmail());
+			setType(User.GOOGLE);
+		} else {
+			setUsername(User.OIDC + ":" + profile.getId());
+			setType(User.OIDC);
 		}
-		setUsername(profile.getEmail());
-		setType(User.GOOGLE);
 	}
 
 	public User(TwitterProfile profile) {
diff --git a/src/main/java/de/thm/arsnova/services/UserService.java b/src/main/java/de/thm/arsnova/services/UserService.java
index f690cb434..d33734baf 100644
--- a/src/main/java/de/thm/arsnova/services/UserService.java
+++ b/src/main/java/de/thm/arsnova/services/UserService.java
@@ -32,7 +32,7 @@ import org.apache.commons.lang.RandomStringUtils;
 import org.apache.commons.lang.StringUtils;
 import org.pac4j.oauth.profile.facebook.FacebookProfile;
 import org.pac4j.oauth.profile.twitter.TwitterProfile;
-import org.pac4j.oidc.profile.google.GoogleOidcProfile;
+import org.pac4j.oidc.profile.OidcProfile;
 import org.pac4j.springframework.security.authentication.Pac4jAuthenticationToken;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -257,8 +257,8 @@ public class UserService implements IUserService {
 	private User getOAuthUser(final Authentication authentication) {
 		User user = null;
 		final Pac4jAuthenticationToken token = (Pac4jAuthenticationToken) authentication;
-		if (token.getProfile() instanceof GoogleOidcProfile) {
-			final GoogleOidcProfile profile = (GoogleOidcProfile) token.getProfile();
+		if (token.getProfile() instanceof OidcProfile) {
+			final OidcProfile profile = (OidcProfile) token.getProfile();
 			user = new User(profile);
 		} else if (token.getProfile() instanceof TwitterProfile) {
 			final TwitterProfile profile = (TwitterProfile) token.getProfile();
diff --git a/src/main/resources/arsnova.properties.example b/src/main/resources/arsnova.properties.example
index 93d8c87c9..51d64b6d2 100644
--- a/src/main/resources/arsnova.properties.example
+++ b/src/main/resources/arsnova.properties.example
@@ -142,6 +142,17 @@ security.cas.image=
 security.cas.order=0
 security.cas-server-url=https://example.com/cas
 
+# OpenID Connect authentication
+#
+security.oidc.enabled=false
+security.oidc.allowed-roles=speaker,student
+security.oidc.title=OIDC
+security.oidc.image=
+security.oidc.order=0
+security.oidc.issuer=https://example.com/oidc
+security.oidc.client-id=
+security.oidc.secret=
+
 # OAuth authentication with third party services
 # Specific parameters:
 # key: OAuth key/id provided by a third party auth service
diff --git a/src/test/resources/arsnova.properties.example b/src/test/resources/arsnova.properties.example
index 93d8c87c9..51d64b6d2 100644
--- a/src/test/resources/arsnova.properties.example
+++ b/src/test/resources/arsnova.properties.example
@@ -142,6 +142,17 @@ security.cas.image=
 security.cas.order=0
 security.cas-server-url=https://example.com/cas
 
+# OpenID Connect authentication
+#
+security.oidc.enabled=false
+security.oidc.allowed-roles=speaker,student
+security.oidc.title=OIDC
+security.oidc.image=
+security.oidc.order=0
+security.oidc.issuer=https://example.com/oidc
+security.oidc.client-id=
+security.oidc.secret=
+
 # OAuth authentication with third party services
 # Specific parameters:
 # key: OAuth key/id provided by a third party auth service
-- 
GitLab