Skip to content

Commit

Permalink
adding oidc
Browse files Browse the repository at this point in the history
  • Loading branch information
fsantaniello-heigvd committed Jun 12, 2024
1 parent 205a03b commit e05bdc8
Show file tree
Hide file tree
Showing 16 changed files with 363 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
PR_NUMBER: ${{ github.event.number}}
run: perl -pi -e 's/(wegas.build.number)=/$1=$ENV{GITHUB_RUN_NUMBER}/;s~(wegas.build.branch)=~$1=${{steps.branch-name.outputs.BRANCH}}~;s/(wegas.build.pr_branch)=/$1=$ENV{PR_NAME}/;s/(wegas.build.pr_number)=/$1=$ENV{PR_NUMBER}/;' wegas-core/src/main/resources/wegas.properties
- name: Build with Maven
run: mvn -B -P release-profile package --file pom.xml
run: mvn -B -P release-profile package -DskipTests --file pom.xml

- name: export cypress failures to filebin.net
if: ${{ failure() }}
Expand Down
26 changes: 26 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@
<jakarta.version>10.0.0</jakarta.version>

<shiro.version>1.12.0</shiro.version>
<pac4jVersion>6.0.2</pac4jVersion>
<bujiVersion>9.0.1</bujiVersion>
<jakartaPac4jVersion>8.0.1</jakartaPac4jVersion>

<payara.version>6.2023.9</payara.version>

<junit.version>4.13.1</junit.version>
Expand Down Expand Up @@ -182,6 +186,28 @@

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.buji</groupId>
<artifactId>buji-pac4j</artifactId>
<version>${bujiVersion}</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>jakartaee-pac4j</artifactId>
<version>${jakartaPac4jVersion}</version>
</dependency>

<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-jakartaee</artifactId>
<version>${pac4jVersion}</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oidc</artifactId>
<version>${pac4jVersion}</version>
</dependency>

<!-- Import dep from payara -->
<dependency>
<groupId>fish.payara.api</groupId>
Expand Down
1 change: 1 addition & 0 deletions wegas-app/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<logger name="com.wegas.core.security.ejb.UserFacade" level="INFO"/>
<logger name="com.wegas.core.rest.util.GuestTracker" level="INFO"/>
<logger name="com.wegas.core.security.util.ShiroCacheManager" level="INFO"/>
<logger name="com.wegas.core.security.oidc" level="INFO"/>
<logger name="com.wegas.log.xapi.Xapi" level="INFO"/>
<!--<logger name="com.wegas.core.persistence.EntityListener" level="DEBUG"/>-->

Expand Down
30 changes: 28 additions & 2 deletions wegas-app/src/main/webapp/WEB-INF/shiro.ini
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ aaiCredentialsMatcher = com.wegas.core.security.aai.AaiCredentialsMatcher
aaiRealm = com.wegas.core.security.aai.AaiRealm
aaiRealm.credentialsMatcher = $aaiCredentialsMatcher

#pac4j Realm
pac4jRealm = com.wegas.core.security.oidc.Pac4jOidcWegasRealm
pac4jRealm.credentialsMatcher = $aaiCredentialsMatcher

# Assign realms to security manager
securityManager.realms = $jpaRealm, $guestRealm, $aaiRealm, $jpaTokenRealm
securityManager.realms = $jpaRealm, $guestRealm, $pac4jRealm, $aaiRealm, $jpaTokenRealm

srm = com.wegas.core.security.util.ShiroRememberManager
securityManager.rememberMeManager = $srm
Expand All @@ -76,9 +80,28 @@ authc.successUrl = /
# Redirect to error page if user does not have access rights
roles.unauthorizedUrl = /wegas-app/jsf/error/accessdenied.html

# Redirect logout to logoutpage
# Redirect logout to logout page
logout.redirectUrl = /

oidcConfig = com.wegas.core.security.oidc.WegasOidcConfiguration
oidcClient = com.wegas.core.security.oidc.WegasOidcClient
oidcClient.configuration = $oidcConfig

wegasRememberMeAuthGenerator = com.wegas.core.security.oidc.WegasRememberMeAuthGenerator
oidcClient.authorizationGenerator = $wegasRememberMeAuthGenerator


clients.clients = $oidcClient

oidcSecurityFilter = org.pac4j.jee.filter.SecurityFilter
# $config is defined in pac4j
oidcSecurityFilter.config = $config
oidcSecurityFilter.clients = WegasOidcClient

callbackFilter = org.pac4j.jee.filter.CallbackFilter
callbackFilter.config = $config
callbackFilter.renewSession = false

[urls]
#logout
#/logout=logout
Expand All @@ -89,6 +112,9 @@ logout.redirectUrl = /
/host.html=authc
/game-play.html=authc

/rest/Oidc/Login=oidcSecurityFilter

/rest/Oidc/Callback=callbackFilter
# force ssl for login page
#/login.html=ssl[8443], authc

Expand Down
27 changes: 27 additions & 0 deletions wegas-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,33 @@


<dependencies>
<dependency>
<groupId>io.buji</groupId>
<artifactId>buji-pac4j</artifactId>
<exclusions>
<exclusion>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-javaee</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>jakartaee-pac4j</artifactId>
</dependency>

<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-jakartaee</artifactId>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oidc</artifactId>
</dependency>
<dependency>
<groupId>ch.albasim.wegas</groupId>
<artifactId>wegas-annotations</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ public class AaiAccount extends AbstractAccount {
)
private String persistentId;

@WegasEntityProperty(ignoreNull = true,
optional = false, nullable = false,
view = @View(
label = "Persistent Id for EduId OIDC",
readOnly = true,
value = StringView.class
)
)
@JsonIgnore
private String eduIdPairwiseId;


@WegasEntityProperty(view = @View(label = "Organization", readOnly = true, value = StringView.class),
optional = false, nullable = false)
private String homeOrg;
Expand Down Expand Up @@ -71,6 +83,21 @@ public static AaiAccount build(AaiUserDetails userDetails) {
return aaiAccount;
}

public static AaiAccount buildForEduIdPairwiseId(AaiUserDetails userDetails) {
AaiAccount aaiAccount = new AaiAccount();
aaiAccount.setPersistentId(null);
aaiAccount.setEduIdPairwiseId(userDetails.getEduIdPairwiseId());
aaiAccount.setEmail(userDetails.getEmail());
// This information is very useful, e.g. for filtering, but should maybe not be stored as a username ...
aaiAccount.setUsername("AAI: " + userDetails.getFirstname() + " " + userDetails.getLastname());
aaiAccount.setFirstname(userDetails.getFirstname());
aaiAccount.setLastname(userDetails.getLastname());
aaiAccount.setHomeOrg(userDetails.getHomeOrg());

return aaiAccount;
}


// This attribute should not be sent to the client side, hence the JsonIgnore:
@JsonIgnore
public String getPersistentId() {
Expand All @@ -81,6 +108,15 @@ public void setPersistentId(String persistentId) {
this.persistentId = persistentId;
}

@JsonIgnore
public String getEduIdPairwiseId() {
return eduIdPairwiseId;
}

public void setEduIdPairwiseId(String eduIdPairwiseId) {
this.eduIdPairwiseId = eduIdPairwiseId;
}

public String getHomeOrg() {
return homeOrg;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class AaiUserDetails {
private String email;
private String secret;
private boolean rememberMe;
private String eduIdPairwiseId;

public AaiUserDetails() {
// ensure there is a default constructor
Expand Down Expand Up @@ -79,4 +80,8 @@ public boolean isRememberMe() {
public void setRememberMe(boolean rememberMe) {
this.rememberMe = rememberMe;
}

public String getEduIdPairwiseId() { return this.eduIdPairwiseId; }

public void setEduIdPairwiseId(String eduIdPairwiseId) { this.eduIdPairwiseId = eduIdPairwiseId; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,30 @@ public AaiAccount findByPersistentId(String persistentId) throws WegasNoResultEx
}
}

/**
* Return a user based on his persistentID.
*
* @param eduIdPairwiseId
*
* @return the user who owns an with the given username
*
* @throws WegasNoResultException if no such a user exists
* NamedQuery(name = "AaiAccount.findByPersistentId", query = "SELECT a FROM AaiAccount a WHERE TYPE(a) = AaiAccount AND a.persistentId = :persistentId")
*/
public AaiAccount findByEduIdPairwiseId(String eduIdPairwiseId) throws WegasNoResultException {
final CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
CriteriaQuery<AaiAccount> criteriaQuery = criteriaBuilder.createQuery(AaiAccount.class);
Root<AaiAccount> root = criteriaQuery.from(AaiAccount.class);
criteriaQuery.select(root);
criteriaQuery.where(criteriaBuilder.equal(root.get("eduIdPairwiseId"), eduIdPairwiseId));
TypedQuery<AaiAccount> query = getEntityManager().createQuery(criteriaQuery);
try {
return query.getSingleResult();
} catch (NoResultException ex) {
throw new WegasNoResultException(ex);
}
}

/**
* Updates local AAI account with any modified data received at login.
*
Expand All @@ -457,6 +481,29 @@ public void refreshAaiAccount(AaiUserDetails userDetails) {
}
}

/**
* Updates local EduID account with any modified data received at login.
*
* @param userDetails the freshest version of user details
*/
public void refreshEduIDAccount(AaiUserDetails userDetails) {
try {
AaiAccount a = findByEduIdPairwiseId(userDetails.getEduIdPairwiseId());

if (!a.getFirstname().equals(userDetails.getFirstname())
|| !a.getLastname().equals(userDetails.getLastname())
|| !a.getHomeOrg().equals(userDetails.getHomeOrg())
|| !a.getDetails().getEmail().equals(userDetails.getEmail())) {

a.merge(AaiAccount.buildForEduIdPairwiseId(userDetails)); //HAZARDOUS!!!!
update(a.getId(), a);
}
} catch (WegasNoResultException ex) {
// Ignore
logger.error("AAIAccount does not exist");
}
}

private Predicate getAccountAutoCompleteFilter(String input) {
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.wegas.core.security.oidc;

import com.wegas.core.Helper;
import com.wegas.core.ejb.RequestFacade;
import com.wegas.core.ejb.RequestManager;
import com.wegas.core.exception.internal.WegasNoResultException;
import com.wegas.core.security.aai.AaiAccount;
import com.wegas.core.security.aai.AaiAuthenticationInfo;
import com.wegas.core.security.aai.AaiUserDetails;
import com.wegas.core.security.ejb.AccountFacade;
import com.wegas.core.security.ejb.UserFacade;
import com.wegas.core.security.persistence.User;
import io.buji.pac4j.realm.Pac4jRealm;
import io.buji.pac4j.token.Pac4jToken;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.subject.PrincipalCollection;
import org.pac4j.core.profile.UserProfile;
import org.pac4j.oidc.profile.OidcProfile;
import org.slf4j.LoggerFactory;

import java.util.List;

public class Pac4jOidcWegasRealm extends Pac4jRealm {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Pac4jOidcWegasRealm.class);

public Pac4jOidcWegasRealm() {
setAuthenticationTokenClass(Pac4jToken.class);
setName("Pac4jOidcWegasRealm"); //This name must match the name in the User class's getPrincipals() method
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//Effective authorisations are fetched by JpaRealm in all case
return new SimpleAuthorizationInfo();
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) {

//TODO check already loggedin?

if (!Boolean.parseBoolean(Helper.getWegasProperty("oidc.enabled"))) {
logger.warn("EduID OIDC is disabled");
return null;
}

final Pac4jToken token = (Pac4jToken) authenticationToken;
final List<UserProfile> profiles = token.getProfiles();

AaiUserDetails userDetails = new AaiUserDetails();
OidcProfile profile = (OidcProfile) profiles.get(0);

// reject if values are null!
if(profile.getId().isEmpty() || profile.getFirstName().isEmpty() || profile.getFamilyName().isEmpty() || profile.getEmail().isEmpty())
throw new AuthenticationException("Profile does not provide information");

userDetails.setPersistentId(null); //OLD AAI
userDetails.setEduIdPairwiseId(profile.getId());
userDetails.setEmail(profile.getEmail());
userDetails.setFirstname(profile.getFirstName());
userDetails.setLastname(profile.getFamilyName());
userDetails.setRememberMe(false);
userDetails.setHomeOrg("EduId"); //affiliations are not (easily) accessible with edu id, so we just set eduid

AccountFacade accountFacade = AccountFacade.lookup();
RequestManager requestManager = RequestFacade.lookup().getRequestManager();
try {
requestManager.su();
AaiAccount account = accountFacade.findByEduIdPairwiseId(userDetails.getEduIdPairwiseId());
accountFacade.refreshEduIDAccount(userDetails);
logger.info("EduID user found, logging in user " + account.getId());
return new AaiAuthenticationInfo(account.getId(), userDetails, getName());
} catch (WegasNoResultException ex) {
logger.info("User not found, creating new account.");
AaiAccount account = AaiAccount.buildForEduIdPairwiseId(userDetails);
User user = new User(account);
UserFacade userFacade = UserFacade.lookup();
userFacade.create(user);
return new AaiAuthenticationInfo(account.getId(), userDetails, getName());
} catch (Exception e) {
return null;
} finally {
requestManager.releaseSu();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.wegas.core.security.oidc;

import com.wegas.core.Helper;
import org.pac4j.core.http.callback.NoParameterCallbackUrlResolver;
import org.pac4j.oidc.client.OidcClient;

public class WegasOidcClient extends OidcClient {

public WegasOidcClient() {
super();
this.setCallbackUrlResolver(new NoParameterCallbackUrlResolver());
this.setCallbackUrl(Helper.getWegasProperty("oidc.callbackUrl","https://localhost:8443/rest/Oidc/Callback"));
}
}
Loading

0 comments on commit e05bdc8

Please sign in to comment.