Skip to content

Commit

Permalink
feat: add accts linking agama project #7556
Browse files Browse the repository at this point in the history
Signed-off-by: jgomer2001 <bonustrack310@gmail.com>
  • Loading branch information
jgomer2001 committed Mar 18, 2024
1 parent 9be78e4 commit 6185bed
Show file tree
Hide file tree
Showing 12 changed files with 1,211 additions and 0 deletions.
737 changes: 737 additions & 0 deletions jans-casa/plugins/acct-linking/extras/Casa.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
Flow io.jans.casa.acctlinking.Launcher
Basepath ""
Configs providers
Inputs providerId uidRef

provider = providers.$providerId
//See class io.jans.inbound.Provider for reference

When provider is null or provider.enabled is false
msg = Call java.lang.String#format "Provider '%s' not recognized. Is it enabled?" providerId
obj = { success: false, error: msg }
Finish obj

//Launch matching flow and retrieve profile
Log "Initiating external authentication for identity provider '%'" providerId
obj = Trigger $provider.flowQname provider

When obj.success is false
Finish obj

field = Call io.jans.inbound.Utils#getMappingField provider.mappingClassField
idProc = Call io.jans.inbound.IdentityProcessor#new provider
profile = Call idProc applyMapping obj.data field

field = null
//In profile, every key is associated to a list
Log "@d Mapped profile is\n" profile

When profile.ID is null or profile.ID.empty is true
obj = { success: false, error: "Mapped profile misses value for 'ID'" }
Finish obj

When profile.mail is null or profile.mail.empty is true
Log "Incoming user has no e-mail value"

//Prompt for e-mail if necessary
When provider.requestForEmail is true
obj = RRF "email-prompt.ftlh"
mail = obj.email

When mail is null
obj = { success: false, error: "Unable to complete profile data: e-mail not provided" }
Finish obj

profile.mail = [ mail ]

jansExtUid = Call io.jans.casa.acctlinking.UidUtils#computeExtUid providerId profile.ID[0]
uid = null

When profile.uid is not null
uid = profile.uid[0]

uid = Call io.jans.casa.acctlinking.UidUtils#lookupUid uidRef uid profile.ID[0] "jansExtUid" jansExtUid
profile.jansExtUid = Call io.jans.casa.acctlinking.UidUtils#attrValuesAdding uid "jansExtUid" jansExtUid

profile.uid = [ uid ]
profile.ID = null //ID not part of DB schema - jansExtUid has what is needed

uid | E = Call idProc process profile
When E is null
Finish uid

Log "@e Unable to process the incoming user" E
obj = { success: false, error: E.message }
Finish obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.jans.casa.acctlinking;

import java.util.function.UnaryOperator;
import java.util.HashMap;
import java.util.Map;

/**
* Fields of this class can be referenced in the config properties of flow
* io.jans.casa.acctlinking.Launcher
*/
public final class Mappings {

public static final UnaryOperator<Map<String, Object>> GOOGLE =

profile -> {
Map<String, Object> map = new HashMap<>();

map.put("ID", profile.get("sub"));
map.put("mail", profile.get("email"));
map.put("cn", profile.get("name"));
map.put("sn", profile.get("family_name"));
map.put("displayName", profile.get("given_name"));
map.put("givenName", profile.get("given_name"));

return map;
};

//See https://developers.facebook.com/docs/graph-api/reference/user
public static final UnaryOperator<Map<String, Object>> FACEBOOK =

profile -> {
Map<String, Object> map = new HashMap<>();

map.put("ID", profile.get("id"));
map.put("mail", profile.get("email"));
map.put("cn", profile.get("name"));
map.put("sn", profile.get("last_name"));
map.put("displayName", profile.get("first_name"));
map.put("givenName", profile.get("first_name"));

return map;
};

public static final UnaryOperator<Map<String, Object>> APPLE =

profile -> {
Map<String, Object> map = new HashMap<>();

map.put("ID", profile.get("sub"));
map.put("mail", profile.get("email"));

return map;
};

//See https://docs.github.com/en/rest/users/users
public static final UnaryOperator<Map<String, Object>> GITHUB =

profile -> {
Map<String, Object> map = new HashMap<>();

map.put("ID", profile.getOrDefault("login", profile.get("id")));
map.put("mail", profile.get("email"));
map.put("displayName", profile.get("name"));
map.put("givenName", profile.get("name"));

return map;
};

private Mappings() { }

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

import io.jans.as.common.model.common.User;
import io.jans.as.common.service.common.UserService;
import io.jans.service.cache.CacheProvider;
import io.jans.service.cdi.util.CdiUtil;

import java.io.IOException;
import java.util.*;

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

public class UidUtils {

private static final Logger logger = LoggerFactory.getLogger(Utils.class);

public static String lookupUid(String uidRef, String uid, String extUid, String jansExtAttrName,
String jansExtUid) throws IOException {

if (uidRef == null) {
boolean uidPassed = uid != null;

if (uidPassed) {
logger.debug("Using uid passed: {}", uid);
return uid;
}

//Find if the external account is already linked to a local one
User user = CdiUtil.bean(UserService.class).getUserByAttribute(jansExtAttrName, jansExtUid, true);
if (user == null) {
logger.info("Building a uid based on external id {}", extUid);
return extUid + "-" + randSuffix(3);
}
logger.info("Using uid of the account already linked to {}", extUid);
return user.getUserId();
}

logger.debug("Looking up uid ref {}", uidRef);
Object value = CdiUtil.bean(CacheProvider.class).get(uidRef);
if (value == null) throw new IOException("uid reference passed not found in Cache!");

return value.toString();

}

public static List<String> attrValuesAdding(String uid, String attributeName, String valueToAdd) {

User user = CdiUtil.bean(UserService.class).getUserByAttribute("uid", uid, false);
if (user == null) return Collections.singletonList(valueToAdd);

List<String> values = new ArrayList<>();
List<String> currentValues = Optional.ofNullable(user.getAttributeValues(attributeName))
.orElse(Collections.emptyList());
values.addAll(currentValues);

if (!currentValues.contains(valueToAdd)) {
values.add(valueToAdd);
}
return values;

}

public static String computeExtUid(String providerId, String id) {
return providerId + ":" + id;
}

private Utils() { }

// The idea here is to generate a random 3-char lengthed string "easy to remember"
private static String randSuffix(int randSuffixLen) {

String s = "";
int radix = Math.min(15, Character.MAX_RADIX); //radix 15 entails characters: 0-9 plus a-e

for (int i = 0; i < randSuffixLen; i++) {
long rnd = Math.random() * radix; // rnd will belong to [0, radix - 1]
s += Integer.toString((int) rnd, radix); // this adds a single character to s
}
return s;
}

}
58 changes: 58 additions & 0 deletions jans-casa/plugins/acct-linking/extras/agama/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"projectName": "casa-account-linking",
"author": "jgomer2001",
"type": "Community",
"version": "1.0.0",
"description": "A helper project for Jans Casa accounts linking plugin",
"noDirectLaunch": [ ],
"configs": {
"io.jans.casa.acctlinking.Launcher": {

"facebook": {
"flowQname": "io.jans.inbound.GenericProvider",
"displayName": "Facebook",
"mappingClassField": "io.jans.casa.acctlinking.Mappings.FACEBOOK",
"logoImg": "facebook.png",
"oauthParams": {
"authzEndpoint": "https://www.facebook.com/v14.0/dialog/oauth",
"tokenEndpoint": "https://graph.facebook.com/v14.0/oauth/access_token",
"userInfoEndpoint": "https://graph.facebook.com/v14.0/me",
"clientId": "<APP-ID>",
"clientSecret": "<APP-SECRET>",
"scopes": ["email", "public_profile"]
}
},

"github": {
"flowQname": "io.jans.inbound.GenericProvider",
"displayName": "Github",
"mappingClassField": "io.jans.casa.acctlinking.Mappings.GITHUB",
"oauthParams": {
"authzEndpoint": "https://github.com/login/oauth/authorize",
"tokenEndpoint": "https://github.com/login/oauth/access_token",
"userInfoEndpoint": "https://api.github.com/user",
"clientId": "<APP-ID>",
"clientSecret": "<APP-SECRET>",
"scopes": ["user"]
}
},

"google": {
"flowQname": "io.jans.inbound.GenericProvider",
"displayName": "Google",
"mappingClassField": "io.jans.casa.acctlinking.Mappings.GOOGLE",
"enabled": false,
"skipProfileUpdate": true,
"oauthParams": {
"authzEndpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"tokenEndpoint": "https://oauth2.googleapis.com/token",
"userInfoEndpoint": "https://www.googleapis.com/oauth2/v3/userinfo",
"clientId": "<APP-ID>",
"clientSecret": "<APP-SECRET>",
"scopes": ["email", "profile"]
}
}

}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions jans-casa/plugins/acct-linking/extras/agama/web/email-prompt.ftlh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="${webCtx.contextPath}/servlet/favicon" type="image/x-icon">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<style>
#logo {
max-height: 3.25rem;
margin: 0.5rem;
}
</style>
</head>
<body>

<div class="d-flex flex-column align-items-center justify-content-between min-vh-100 w-100">
<header class="d-flex w-100 justify-content-between border-bottom">
<img id="logo" src="https://gluu.org/wp-content/uploads/2021/02/janssen-project-transparent-630px-182px-300x86.png" />
</header>

<div class="row col-sm-10 col-md-5 mb-5 pb-3">

<div class="border border-1 rounded mb-3 p-5">
<p class="fs-5 mb-5">Please provide an e-mail address to proceed</p>

<form method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-4 row">
<label for="email" class="col-md-3 col-form-label">Your e-mail</label>
<div class="col-md-9">
<input type="email" class="form-control" name="email" id="email" autofocus required>
</div>
</div>
<div class="row">
<div class="col-md-12 d-flex justify-content-end">
<input type="submit" class="btn btn-success px-4" value="Continue">
</div>
</div>
</form>
</div>

</div>

<footer class="d-flex flex-column align-items-center w-100 pb-2">
<hr class="w-75">
</footer>
</div>

</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 6185bed

Please sign in to comment.