Skip to content

Commit

Permalink
feat: add acct linking plugin #7556
Browse files Browse the repository at this point in the history
  • Loading branch information
jgomer2001 committed Mar 18, 2024
1 parent 6185bed commit 145dae1
Show file tree
Hide file tree
Showing 16 changed files with 971 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public boolean isPassResetAvailable() {

public void reloadStatus() {

passSetAvailable = false; //Setting user's password is a disabled feature in Jans Casa
IdentityPerson p = persistenceService.get(IdentityPerson.class, persistenceService.getPersonDn(asco.getUser().getId()));
passSetAvailable = !p.hasPassword();
passResetAvailable = p.hasPassword() && confSettings.isEnablePassReset();

}
Expand Down
109 changes: 109 additions & 0 deletions jans-casa/plugins/acct-linking/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>io.jans.casa.plugins</groupId>
<artifactId>${plugin.id}</artifactId>
<version>1.1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<plugin.id>acct-linking</plugin.id>
</properties>

<repositories>
<repository>
<id>jans</id>
<name>Janssen project repository</name>
<url>https://maven.jans.io/maven</url>
</repository>
</repositories>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Plugin-Id>${plugin.id}</Plugin-Id>
<Plugin-Version>${project.version}</Plugin-Version>
<Plugin-Provider>Janssen project</Plugin-Provider>
<Plugin-Class>io.jans.casa.plugins.acctlinking.AccountsLinkingPlugin</Plugin-Class>
<Plugin-Description>
Allows the user to link their external identities (social sites and other OAuth providers)
to his local Jans account
</Plugin-Description>
<Plugin-License>Available under Apache 2 license</Plugin-License>
<Logger-Name>io.jans.casa.plugins</Logger-Name>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.2.0</version>
</plugin>
</plugins>
</build>

<dependencies>

<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>11.7</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.jans</groupId>
<artifactId>agama-inbound</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>io.jans</groupId>
<artifactId>jans-core-cache</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jans</groupId>
<artifactId>casa-shared</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jans</groupId>
<artifactId>casa-config</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.jans.casa.plugins.acctlinking;

import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AccountsLinkingPlugin extends Plugin {

private Logger logger = LoggerFactory.getLogger(getClass());

public AccountsLinkingPlugin(PluginWrapper wrapper) throws Exception {
super(wrapper);
}

@Override
public void start() {
}

@Override
public void delete() {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package io.jans.casa.plugins.acctlinking;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.ParseException;

import io.jans.casa.conf.OIDCClientSettings;
import io.jans.casa.core.model.IdentityPerson;
import io.jans.casa.misc.Utils;
import io.jans.casa.model.ApplicationConfiguration;
import io.jans.casa.service.IPersistenceService;
import io.jans.inbound.Provider;

import java.io.IOException;
import java.net.*;
import java.util.*;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.nio.charset.StandardCharsets.UTF_8;

public class AccountsLinkingService {

public static final String CASA_AGAMA_FLOW = "io.jans.casa.acctlinking.Launcher";

public static final String READ_SCOPE = "https://jans.io/oauth/config/agama.readonly";

private static final String AGAMA_PRJ = "casa-account-linking";

private static final String CONFIGS_ENDPOINT =
"/jans-config-api/api/v1/agama-deployment/configs/" + AGAMA_PRJ;

private Logger logger = LoggerFactory.getLogger(getClass());

private static AccountsLinkingService instance;
private IPersistenceService ips;
private ObjectMapper mapper;

private OIDCClientSettings clSettings;
private String issuer;
private String basicAuthnHeader;

public static AccountsLinkingService getInstance() {
if (instance == null) {
instance = new AccountsLinkingService();
}
return instance;
}

public OIDCClientSettings getCasaClient() {
return clSettings;
}

public Map<String, Provider> getProviders(boolean enabledOnly) throws Exception {

HTTPRequest request = new HTTPRequest(HTTPRequest.Method.GET,
new URL(issuer + CONFIGS_ENDPOINT));
setTimeouts(request);
request.setAuthorization(basicAuthnHeader);
request.setAuthorization("Bearer " + getAToken());

HTTPResponse r = request.send();
r.ensureStatusCode(200);

Map<String, Map<String, Provider>> madam = mapper.readValue(
r.getBody(), new TypeReference<Map<String, Map<String, Provider>>>(){});

Map<String, Provider> madman = Optional.ofNullable(madam)
.map(m -> m.get(CASA_AGAMA_FLOW)).orElse(Collections.emptyMap());

if (enabledOnly) {
madman = madman.entrySet().stream().filter(e -> e.getValue().isEnabled())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
return madman;

}

public Map<String, String> getAccounts(String userId, Set<String> knownProviders) {

Map<String, String> accts = new HashMap<>();
for (String extUid : getPerson(userId).getJansExtUid()) {
//See method computeExtUid
int i = extUid.indexOf(":");

if (i > 0 && i < extUid.length() - 1) {
String pref = extUid.substring(0, i);
if (knownProviders.contains(pref)) {
accts.put(pref, extUid.substring(i + 1));
}
}
}
return accts;

}

/*
//linking occurs at the Agama flow
public boolean link(String userId, String providerId, String extUid) {
try {
IdentityPerson p = getPerson(userId);
List<String> extUids = new ArrayList<>(p.getJansExtUid());
extUids.add(computeExtUid(providerId, extUid));
p.setJansExtUid(extUids);
ips.modify(p);
return true;
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return false;
}*/

public boolean delink(String userId, String providerId, String extUid) {

try {
IdentityPerson p = getPerson(userId);
List<String> extUids = new ArrayList<>(p.getJansExtUid());
extUids.remove(computeExtUid(providerId, extUid));

p.setJansExtUid(extUids);
ips.modify(p);
return true;
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return false;

}

public boolean hasPassword(String id) {
return getPerson(id).hasPassword();
}

private IdentityPerson getPerson(String id) {
return ips.get(IdentityPerson.class, ips.getPersonDn(id));
}

private String computeExtUid(String providerId, String id) {
//This method HAS to match computeExtUid in class io.jans.agama.inbound.Utils
return providerId + ":" + id;
}

private AccountsLinkingService() {
logger.info("Initializing AccountsLinkingService");
mapper = new ObjectMapper();

ips = Utils.managedBean(IPersistenceService.class);
issuer = ips.getIssuerUrl();
logger.debug("Issuer is {}", issuer);

clSettings = ips.get(ApplicationConfiguration.class, "ou=casa,ou=configuration,o=jans")
.getSettings().getOidcSettings().getClient();

String authz = clSettings.getClientId() + ":" + clSettings.getClientSecret();
authz = new String(Base64.getEncoder().encode(authz.getBytes(UTF_8)), UTF_8);
basicAuthnHeader = "Basic " + authz;

}

private String getAToken() throws IOException {

StringJoiner joiner = new StringJoiner("&");
Map.of("grant_type", "client_credentials", "scope", URLEncoder.encode(READ_SCOPE, UTF_8))
.forEach((k, v) -> joiner.add(k + "=" + v));

logger.info("Calling token endpoint");

HTTPRequest request = new HTTPRequest(
HTTPRequest.Method.POST, new URL(issuer + "/jans-auth/restv1/token"));
setTimeouts(request);
request.setQuery(joiner.toString());
request.setAuthorization(basicAuthnHeader);

try {
Map<String, Object> jobj = request.send().getContentAsJSONObject();
logger.info("Successful call");
return jobj.get("access_token").toString();
} catch (Exception e) {
throw new IOException(e.getMessage(), e);
}

}

private void setTimeouts(HTTPRequest request) {
request.setConnectTimeout(3500);
request.setReadTimeout(3500);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.jans.casa.plugins.acctlinking;

import io.jans.casa.extension.navigation.MenuType;
import io.jans.casa.extension.navigation.NavigationMenu;
import org.pf4j.Extension;

@Extension
public class AdminMenu implements NavigationMenu {

public String getContentsUrl() {
return "admin/menu.zul";
}

public MenuType menuType() {
return MenuType.ADMIN_CONSOLE;
}

public float getPriority() {
return 0.1f;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.jans.casa.plugins.acctlinking;

import io.jans.casa.extension.navigation.MenuType;
import io.jans.casa.extension.navigation.NavigationMenu;
import org.pf4j.Extension;

@Extension
public class UserMenu implements NavigationMenu {

public String getContentsUrl() {
return "user/menu.zul";
}

public MenuType menuType() {
return MenuType.USER;
}

public float getPriority() {
return 0.1f;
}

}

Loading

0 comments on commit 145dae1

Please sign in to comment.