Created
October 4, 2019 18:50
-
-
Save sohalloran/d1a6637524479b3579311ab3bd805a21 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Run Client App Auth Flow - as follows | |
1. Request a JWT to be created for a Community user from an IDP org with a csid | |
2. Pass the JWT to the SP org for an access token using the Oauth JWT bearer flow | |
3. If we get an 'invalid_grant' then send the JWT to a registration service for Just In Time user provisioning | |
4. On success retry the Oauth JWT bearer flow (step 2) | |
5. On success verify it works with an example API call to query the user object | |
- Components | |
- NodeJS App to Simulate a Client App | |
- IDP Org | |
- JWT Generation Web Service | |
- Connected App for access to JWT Generatin Service | |
- Certificate to sign the JWT | |
- SP Org | |
- Connected App | |
- with digital signature verification using the IDP certificate | |
- with default profiles added as admin pre approved | |
- A static resource containing the IDP Certificate public key | |
- Community User Registion Web Service | |
- Community Registration Handler | |
*/ | |
/* RELATED APEX - IDP ORG | |
@RestResource(urlMapping='/JWTService/*') | |
global class JWTService { | |
static String CLIENT_ID = 'SP-CONNECTED-APP-CLIENT-ID'; | |
static String COMMUNITY_DOMAIN = 'https://DOMAIN.force.com'; | |
static String CERT_NAME = 'CERT_NAME'; | |
@HttpGet | |
global static String doGet() { | |
RestRequest req = RestContext.request; | |
String csid = req.params.get('csid'); | |
String userName = req.params.get('username'); | |
String lastname = req.params.get('lastname'); | |
Map <String, String> claims = new Map <String, String>(); | |
claims.put('lastname',lastname); | |
claims.put('csid',csid); | |
Auth.JWT jwt = new Auth.JWT(); | |
jwt.setIss(CLIENT_ID); | |
jwt.setSub(userName); | |
jwt.setAdditionalClaims(claims); | |
jwt.setAud(COMMUNITY_DOMAIN); | |
jwt.setValidityLength (300); | |
Auth.JWS jws = new Auth.JWS(jwt, CERT_NAME); | |
String token = jws.getCompactSerialization(); | |
return token; | |
} | |
} | |
*/ | |
/* RELATED APEX - SP ORG | |
// NEEDS A PUBLIC KEY FROM IDP ORG - SIGNING CERTIFICATE in a Static Resource | |
@RestResource(urlMapping='/UserRegistrationService/*') | |
global class UserRegistrationService { | |
static String PUBLIC_KEY = ''; | |
static String ALG = 'RSA-SHA256'; | |
static String COMMUNITY_ID = ''; | |
@HttpPost | |
global static String doPost() { | |
String token = RestContext.request.params.get('token'); | |
if(verifyJWT(token)==false) { | |
throw new SecurityException ('Invalid JWT - Verification Failed'); | |
} | |
Auth.UserData user = parseJWT(token); | |
boolean foundUser = userExists(user.username); | |
if(!foundUser) { | |
RegHandler reg = new RegHandler(); | |
User u = reg.createUser(COMMUNITY_ID, user); | |
insert u; | |
} | |
return JSON.serialize('{ foundUser:'+foundUser+'}'); | |
} | |
private static Auth.UserData parseJWT(String token) { | |
String body = token.split('\\.')[1]; | |
Blob tokenDecoded = EncodingUtil.base64Decode(body); | |
Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped(tokenDecoded.toString()); | |
String username = (String)m.get('sub'); | |
String lastname = (String)m.get('lastname'); | |
String csid = (String)m.get('csid'); | |
Map<String,String> provMap = new Map<String,String>(); | |
provMap.put('csid', csid); | |
Auth.UserData data = new Auth.UserData('id', '', lastname, '', username, 'what', null, null, 'IDP', null, provMap); | |
return data; | |
} | |
private static boolean verifyJWT (String token) { | |
String [] parts = token.split('\\.'); | |
String header = parts[0]; | |
String body = parts[1]; | |
String signature = parts[2]; | |
if(PUBLIC_KEY=='') { // Get the Key from a Static Resource (does not contain the header and footer markers ---) | |
StaticResource sr = [SELECT Id, Body FROM StaticResource WHERE Name = 'idpPublicKey' LIMIT 1]; | |
PUBLIC_KEY = sr.Body.toString(); | |
} | |
boolean verified = Crypto.verify(ALG, Blob.valueOf(header +'.' + body), base64URLdecode(signature), EncodingUtil.base64Decode(PUBLIC_KEY)); | |
return verified; | |
} | |
private static boolean userExists(String name) { | |
return [select count() from user where username=:name] > 0; | |
} | |
private static Id registerUser(String email, String lastname, String csid) { | |
Map<String,String> provMap = new Map<String,String>(); | |
provMap.put('csid', csid); | |
Auth.UserData data = new Auth.UserData('id', '', lastname, '', email, 'what', null, null, 'IDP', null, provMap); | |
RegHandler reg = new RegHandler(); | |
User u = reg.createUser(COMMUNITY_ID, data); | |
insert u; | |
return u.Id; | |
} | |
private static Blob base64URLdecode(String input){ | |
Blob output = EncodingUtil.base64Decode(input | |
.replace('-', '+') | |
.replace('_', '/') | |
.rightPad(math.mod(input.length() + (math.mod(4 - input.length(), 4)), 4)) | |
.replace(' ','=')); | |
return output; | |
} | |
}*/ | |
/* RELATED APEX - SP ORG | |
global class RegHandler implements Auth.RegistrationHandler{ | |
String DEFAULT_ACCOUNT = 'Default Account'; | |
String DEFAULT_PROFILE = 'CC+'; | |
global boolean canCreateUser(Auth.UserData data) { | |
return true; | |
} | |
global User createUser(Id portalId, Auth.UserData data){ | |
String rnd = data.lastName; | |
//We have a community id, so create a user with community access | |
//TODO: Get an actual account | |
Account a = [SELECT Id FROM account WHERE name=:DEFAULT_ACCOUNT]; | |
Contact c = new Contact(); | |
c.accountId = a.Id; | |
c.email = data.email; | |
c.firstName = data.firstName; | |
c.lastName = data.lastName; | |
insert(c); | |
User u = new User(); | |
Profile p = [SELECT Id FROM profile WHERE name=:DEFAULT_PROFILE]; | |
u.username = rnd + '@email.com'; | |
u.email = data.email; | |
u.lastName = data.lastName; | |
u.firstName = data.firstName; | |
u.FederationIdentifier = data.attributeMap.get('csid'); | |
String alias = rnd; | |
if(alias.length() > 8) { | |
alias = alias.substring(0, 8); | |
} | |
u.alias = alias; | |
u.languagelocalekey = 'en_US'; | |
u.localesidkey = 'en_GB'; | |
u.emailEncodingKey = 'UTF-8'; | |
u.timeZoneSidKey = 'America/Los_Angeles'; | |
u.profileId = p.Id; | |
u.contactId = c.Id; | |
return u; | |
} | |
global void updateUser(Id userId, Id portalId, Auth.UserData data){ | |
User u = new User(id=userId); | |
u.email = data.email; | |
u.lastName = data.lastName; | |
u.firstName = data.firstName; | |
update(u); | |
} | |
} | |
*/ | |
// CLIENT APP - e.g. MOBILE | |
const axios = require('axios'); | |
const defaultConfig = { | |
client_id : `IDP-ORG-CLIENT-ID`, | |
refreshToken : 'IDP-ORG-TOKEN', | |
idpDomain : `IDP-DOMAIN`, | |
spDomain : `SP-DOMAIN`, | |
tokenEndpoint : `/services/oauth2/token`, | |
apexEndpoint : `/services/apexrest`, | |
emailDomain : `@email.com`, | |
query : encodeURIComponent(`select username from user where isactive=true`), | |
} | |
async function authFlow({lastname, csid, config=defaultConfig}) { | |
const { client_id, refreshToken, idpDomain, spDomain, tokenEndpoint, apexEndpoint, emailDomain, query} = config; | |
const username = `${lastname}${emailDomain}` | |
const jwtServiceURL = `${idpDomain}${apexEndpoint}/JWTService?username=${username}&lastname=${lastname}&csid=${csid}` | |
const userRegistrationServiceURL = `${spDomain}${apexEndpoint}/UserRegistrationService` | |
const authURL = `${spDomain}${tokenEndpoint}?grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer` | |
const apiURL = `${spDomain}/services/data/v46.0/query?q=${query}` | |
const refreshTokenFlowResponse = await axios.post(`${idpDomain}${tokenEndpoint}?grant_type=refresh_token&refresh_token=${refreshToken}&client_id=${client_id}`) | |
const idp_access_token = refreshTokenFlowResponse.data.access_token; | |
console.log('idp_access_token: ', idp_access_token) | |
const JWTGenerationResponse = await axios.get(jwtServiceURL,{ // Generate JWT with ID Token | |
headers: { | |
'Authorization': `Bearer ${idp_access_token}`, | |
'Content-Type': 'application/json', | |
} | |
}) | |
const JWT = JWTGenerationResponse.data; | |
console.log('JWT', JWT) | |
console.log('JWT:',base64Decode(JWT)) | |
let JWTBearerTokenFlowReponse = ''; | |
try { | |
JWTBearerTokenFlowReponse = await axios.post(`${authURL}&assertion=${JWT}`); | |
console.log('JWTBearerTokenFlowReponse: ',JWTBearerTokenFlowReponse.data) | |
} catch (err) { | |
console.log('JWTBearerTokenFlowReponse status code: ',err.response.status) | |
console.log('JWTBearerTokenFlowReponse error code: ',err.response.data) | |
if(err.response.data.error==='invalid_grant') { | |
try { | |
const userCreatedResponse = await axios.post(`${userRegistrationServiceURL}?token=${JWT}`) | |
console.log('userCreatedResponse: ',userCreatedResponse) | |
JWTBearerTokenFlowReponse = await axios.post(`${authURL}&assertion=${JWT}`); | |
console.log('JWTBearerTokenFlowReponse: ',response.data) | |
} catch (err) { | |
if(err.response) { | |
console.log('userCreatedResponse status code: ',err.response.status) | |
console.log('userCreatedResponse error code: ',err.response.data) | |
} | |
} | |
} | |
} finally { | |
const APICallResponse = await axios.get(apiURL,{ // Example API call | |
headers: { | |
'Authorization': `Bearer ${JWTBearerTokenFlowReponse.data.access_token}`, | |
'Content-Type': 'application/json', | |
} | |
}) | |
console.log('APICallResponse: ',APICallResponse.data) | |
} | |
} | |
function base64Decode(data) { | |
const buff = Buffer.from(data.split('.')[1], 'base64') | |
console.log('JWT Header: ', JSON.parse(Buffer.from(data.split('.')[0], 'base64').toString('utf-8'))) | |
const jwtbody = buff.toString('utf-8'); | |
return JSON.parse(jwtbody); | |
} | |
authFlow({lastname:'Smith', csid:'12345'}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment