Automate user creation and deletion through external API requests - rest

I have 0 experience in coding in APEX so I would greatly appreciate your help and support with this question!
I would like to figure out a way to automate the deletion of an Aircall User if an SF user is deleted. Let us assume that every SF user has an Aircall ID that is present in their User profiles, stored in a field called 'Aircall ID'. This is what I will need to form the delete request.
I want that when a user is deleted on Salesforce, it triggers a delete request to Aircall sending the value that was previously stored in the Aircall ID field to the specific endpoint in question.
I need help figuring out how to write an APEX trigger that sends the Aircall ID to the class (to be triggered after the user is deleted) and finally how to automatically trigger the execution of this class after the ID has been received in order to complete the User deletion on Aircall's platform.
public class deleteAirCallUser {
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setMethod('DELETE');
string encodedCredentials = 'apikey';
String authorizationHeader = 'Basic ' + encodedCredentials;
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
request.setHeader('Authorization', authorizationHeader);
string AircallUserId = //should be the Aircall userID from the deleted profile
request.setBody(AircallUserId);
request.setEndpoint('https://api.aircall.io/v1/users/'+ Aircall userID);
HttpResponse response = http.send(request);
if (response.getStatusCode() == 200) {
Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
System.debug(results);}
else{
Map<String, Object> results_2 = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
System.debug(results_2);
}
}
Thank you for your help!

https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_user.htm
"You can’t delete a user in the user interface or the API. You can deactivate a user in the user interface; and you can deactivate or disable a Customer Portal or partner portal user in the user interface or the API. Because users can never be deleted, we recommend that you exercise caution when creating them."
For deactivations you'll need something like this. (It's not written to best practices, ideally the trigger would be "thin" and actual processing offloaded to helper class. Also it assumes you mass update max 10 users at a time because that's the limit of callouts.
trigger UserTrigger on User (after update){
Set<String> toSend = new Set<String>();
for(User u : trigger.new){
User oldUser = trigger.oldMap.get(u.Id);
// have we deactivated them?
if(!u.isActive && oldUser.isActive && String.isNotBlank(u.AirCallId__c)){
toSend.add(u.AirCallId__c);
}
}
if(!toSend.isEmpty()){
sendAirCallDeletes(toSend);
}
// This should be in a helper class, it looks bizarre to have functions defined in trigger's body
#future
static void sendAirCallDeletes(Set<String> toSend){
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setMethod('DELETE');
String encodedCredentials = 'apikey';
String authorizationHeader = 'Basic ' + encodedCredentials;
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
request.setHeader('Authorization', authorizationHeader);
for(String airCallId : toSend){
request.setBody(airCallId);
request.setEndpoint('https://api.aircall.io/v1/users/'+ airCallId);
try{
HttpResponse response = http.send(request);
System.debug(response.getStatusCode());
System.debug(response.getBody());
System.debug((Map<String, Object>) JSON.deserializeUntyped(response.getBody());
} catch(Exception e){
System.debug(e);
}
}
}
}
You might want to read up about "named credentials" (don't store the api keys etc in code), why we need "#future" trick when we want to make callout from a trigger, how to check for limit of calls you can make in single transaction... But should be a start?

Related

Does Keycloak allow to terminate sessions and keep track of active sessions, failed login attempts?

I need to implement a relatively complex authorization process for a Spring Boot application and consider using Keycloak for this.
Is it possible to do following things using Keycloak (incl. by extending it with custom authentication/authorization mechanisms)?
Keeping track of sessions: Does Keycloak know on how many devices a user is logged in into the application?
Keeping track of failed login attempts: Does Keycloak know how many times a particular user entered an incorrect password?
Terminating sessions: Is it possible to terminate some (but not all) sessions of a user?
My answers
1. Keeping track of session
According to the user manual, section "Managing user sessions", "Viewing client sessions" it is possible to see all active sessions of a user via the web UI.
According to this answer, it is possible to do so programmatically.
2. Keeping track of failed login attempts
According to this page, it may be possible to implement it using a custom event listener.
3. Terminating sessions
It looks like it is possible using the http://auth-server{kc_realms_path}/{realm-name}/protocol/openid-connect/logout endpoint according to documentation and this answer.
Update 1: It looks like items 1 and 2 are indeed possible.
However, I am having trouble with termination of sessions.
This is what I want:
User logs in via Keycloak into a Spring Boot application.
When I terminate the session, the user is logged out of that application.
First, I tried to delete sessions using code like this:
final Keycloak keycloak = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080")
.realm("KeycloakDemo")
.username("postman")
.password("postman2022")
.clientId("postman")
.clientSecret("ZY006ddQbWHdSiAK3A06rrPlKgSz3XS0")
.build();
final UserRepresentation user =
keycloak.realm("KeycloakDemo").users().search("user1").get(0);
final String userId = user.getId();
final UserSessionRepresentation[] sessions = keycloak
.realm("KeycloakDemo")
.users().get(userId).getUserSessions()
.toArray(new UserSessionRepresentation[0]);
if (sessions.length > 0) {
final String sessionId = sessions[0].getId();
keycloak.realm("KeycloakDemo").deleteSession(sessionId);
}
This piece of code deletes sessions (i. e. they are not visible in the Keycloak GUI any longer), but it does not log out the user.
Another attempt was to log out the user after the session was deleted using the following code.
final String token = getToken();
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("text/plain");
RequestBody body = RequestBody.create(mediaType, "");
Request request = new Request.Builder()
.url("http://localhost:8080/realms/KeycloakDemo/protocol/openid-connect/logout?id_token_hint=" + token)
.method("GET", null)
.build();
Response response = client.newCall(request).execute();
getToken() is defined as follows:
private String getToken() throws IOException {
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
RequestBody body = RequestBody.create(mediaType, "client_id=admin-cli&username=postman&password=postman2022&grant_type=client_credentials&scope=openid&realm=KeycloakDemo&client_secret=CMewUzBUsiy0gUqg6uEmCRBgR5p6f5Nu");
Request request = new Request.Builder()
.url("http://localhost:8080/realms/KeycloakDemo/protocol/openid-connect/token")
.method("POST", body)
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addHeader("Authorization", "bearer ... ")
.build();
Response response = client.newCall(request).execute();
if (response.code() != 200) {
System.exit(1);
}
final ObjectMapper om = new ObjectMapper();
final JsonNode jsonNode = om.readTree(response.body().string());
return jsonNode.get("id_token").asText();
}
This does not work, either (the user stays logged in that application, ever if I refresh the page in the browser).

.NETCORE Middleware Read the Body value from the POST entered by a user in Postman AND Validate a particular value (To:) before sent

I am using a .NET CORE API application with Middleware but I need the Post to read the individual values from the Body in Postman AND have the Middleware Validate a particular value in the Body, for example, the (To:) of an email; and, if it meets whatever criteria I would like to (POST) send the email on to the intended recipient (To:) without a database.
This is what my code looks like so far
using (StreamReader streamReader = new StreamReader(httpContext.Request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: bufferSize, leaveOpen: true))
{
await streamReader.ReadToEndAsync();
//Do something
////From test
if (evaluate.IsSenderFromConfiguration() == true)
{
message.From = From;
}
message.To = new List<MailboxAddress> { new MailboxAddress("mail#mail.com")};
message.Subject = "Message From Create Middleware";
message.HtmlBody = "This is just a test of Middleware ";
await SendEmailAsync(message);
httpContext.Request.Body.Position = 0;
}
await httpContext.Response.WriteAsync(body);
await _next.Invoke(httpContext);
}
Here is the Post from the Controller
[HttpPost]
public IActionResult Post([FromBody] Email email)
{
var message = new Email(_message.From, _message.To, _message.Subject, _message.HtmlBody);
if (message == null)
{
return NotFound();
}
return Ok(message);
}
Right now I just have what I did in the Get to send the email. How can I have it to where the properties message.To, message.From, etc. can be set by the User/Me in Postman and if the value in message.To is valid send the email?
I think there is a way to use the httpContext.Request.Body somehow, but I don't know how in .NET Core to set each individual value separately outside of the Middleware class.
To clarify: My Post Controller DOES NOT WORK FOR FORM VALIDATION The Middleware should be doing the work and then passing it to the controller once validated. That is where I need the assistance for the Middleware class.
use FluentValidation
also use in middleware like below:
services.AddMvc(setup => {
//...mvc setup...
}).AddFluentValidation();

In identity server 3 how to relate user to client

I am using Identity Server 3. I have couple applications ie. Client configured and have few users configured. How do i establish the relationship between User and a Client and also view all applications that the selected User has access to.
Update 1
I am sorry if question was confusing. On IdSvr3 home page, there is a link to revoke application permissions. I am guessing in order to revoke the permission you have to first establish the relationship between user and application.
and i wanted to know how to establish that permission when i add new user?
There's no direct way to limit one or multiple users to a certain client. This is where you should think about implementing your own custom validation. Fortunately, the IdentityServer provides an extensibility point for this kind of requirement.
ICustomRequestValidator
You should implement this interface to further validate users to see if they belong to certain clients and filter them out. You can look into the user details by looking at ValidatedAuthorizeRequest.Subject. This custom validator will start after validating optional parameters such as nonce, prompt, arc_values ( AuthenticationContextReference ), login_hint, and etc. The endpoint is AuthorizeEndPointController and the default implementation of the interface for the tailored job is AuthorizeRequestValidator and its RunValidationAsync. You should take a look at the controller and the class.
Implementation tip
By the time the custom request validation begins, a Client reference will be presented in ValidatedAuthorizeRequest. So all you need to do would be matching the client id or some other identifiers you think you need to verify the client. Probably, you might want to add a Claim key-value pair to your client which you want to allow a few users.
Maybe something like this.
new InMemoryUser{Subject = "870805", Username = "damon", Password = "damon",
Claims = new Claim[]
{
new Claim(Constants.ClaimTypes.Name, "Damon Jeong"),
new Claim(Constants.ClaimTypes.Email, "dmjeong#email.com"),
new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean)
}
}
Assume you have above user, then add the subject id to the claim of a client like below.
new Client
{
ClientName = "WPF WebView Client Sample",
ClientId = "wpf.webview.client",
Flow = Flows.Implicit,
.
.
.
// Add claim for limiting this client to certain users.
// Since a claim only accepts type and value as string,
// You can add a list of subject id by comma separated values
// eg ( new Claim("BelongsToThisUser", "870805, 870806, 870807") )
Claims = new List<Claim>
{
new Claim("BelongsToThisUser", "870805")
}
},
And then just implement the ICustomRequestValidator and try to match the Claim value with the given user in its ValidateAuthorizeRequestAsync.
public class UserRequestLimitor : ICustomRequestValidator
{
public Task<AuthorizeRequestValidationResult> ValidateAuthorizeRequestAsync(ValidatedAuthorizeRequest request)
{
var clientClaim = request.Client.claims.Where(x => x.Type == "BelongsToThisUser").FirstOrDefault();
// Check is this client has "BelongsToThisUser" claim.
if(clientClaim != null)
{
var subClaim = request.Subject.Claims.Where(x => x.Type == "sub").FirstOrDefault() ?? new Claim(string.Empty, string.Empty);
if(clientClaim.Value == userClaim.Value)
{
return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult
{
IsError = false
});
}
else
{
return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult
{
ErrorDescription = "This client doesn't have an authorization to request a token for this user.",
IsError = true
});
}
}
// This client has no access controls over users.
else
{
return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult
{
IsError = false
});
}
}
public Task<TokenRequestValidationResult> ValidateTokenRequestAsync(ValidatedTokenRequest request)
{
// your implementation
}
}
Time to DI
You need to inject your own dependency when you configure up your IdentityServer. The authorization server uses IdentityServerServiceFactory for registering dependencies.
var factory = new IdentityServerServiceFactory();
factory.Register(new Registration<ICustomRequestValidator>(resolver => new UserRequestLimitor()));
Then Autofac; the IoC container in IdentityServer will do the rest of the DI jobs for you.

Changing user folder collaborating type in box using Salesforce Toolbox

I'm trying to change Box folder collaboration type for user from salesforce Apex trigger. The first thoughts were to use box.Toolkit but it looks like this class does not have updateCollaboration or changeCollaboration method, only create. I guess my only option is to use Box's Rest API. Is there any way I can get service account token in Apex so I can use it in a callout?
I have created a special "Tokens" object in Salesforce with two fields: access token and refresh token. I then have a batch job that runs to update the access token every 55 minutes such that they never expired.
Here is a code snippet in APEX using the Tokens object.
#future(callout=true)
public static void updateTokens(){
//app info for authenticating
String clientID = 'MY_CLIENT_ID';
String clientSecret = 'MY_CLIENT_SECRET';
//look up value of existing refresh token
Token__c myToken = [SELECT Name, Value__c FROM Token__c WHERE Name='Refresh'];
Token__c myAccessToken = [SELECT Name, Value__c FROM Token__c WHERE Name='Access'];
String refreshToken = myToken.Value__c;
String accessToken = myAccessToken.Value__c;
//variables for storing data
String BoxJSON = '';
String debugTxt = '';
//callout to Box API to get new tokens
HttpRequest reqRefresh = new HttpRequest();
reqRefresh.setMethod('POST');
String endpointRefresh = 'https://www.box.com/api/oauth2/token';
reqRefresh.setEndpoint(endpointRefresh);
String requestBody = ('grant_type=refresh_token&refresh_token=' + refreshToken + '&client_id=' + clientID + '&client_secret=' + clientSecret);
reqRefresh.setBody(requestBody);
System.debug('Body of refresh request: ' + requestBody);
//Create Http, send request
Http httpRefresh = new Http();
Boolean successRefresh = false;
while (successRefresh == false){
try{
HTTPResponse resRefresh = httpRefresh.send(reqRefresh);
BoxJSON = resRefresh.getBody();
System.debug('Body of refresh response: ' + BoxJSON);
successRefresh = true;
}
catch (System.Exception e){
System.debug('Error refreshing: ' + string.valueof(e));
if (Test.isRunningTest()){
successRefresh = true;
}
}
}
Keep in mind that if you are using the Box for Salesforce integration your administrator can set the option for the permissions on the folders to sync with Salesforce permissions. This would reverse any changes you make to collaborations. Check out more about Box's Salesforce integration permissions here: https://support.box.com/hc/en-us/articles/202509066-Box-for-Salesforce-Administration#BfS_admin_perm

No 'Access-Control-Allow-Origin', only errors on first call but works subsequently

I have an AngularJS app which is trying to auth with my Web Api. I receive the below error during the first call to my server if the user does not exist in my database, but does not happen on subsequent calls to the same method once the user exists in my db. (relevant code at the bottom)
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:1378' is therefore not allowed access. The response had HTTP status code 500.
The flow of the logic is:
AngularJS auths with Facebook when the user clicks login
App does an $http.post to my server for auth/login passing their credentials
Server polls Facebook API for user details
If user exists, update their profile and auth 'em
Else, create new membership user, update with FB details, and auth 'em
The only thing that's different if they don't exist in the database (which is when the defect occurs) is that the login method asynchronously calls a createUser method then returns data. No additional external calls are made.
API startup method enabling CORS:
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
var cors = new EnableCorsAttribute("*","*","*");
config.EnableCors(cors);
ConfigureOAuth(app);
app_start.WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
API Controller:
[Route("Login")]
[HttpPost]
[AllowAnonymous]
public async Task<FacebookUserModel> Login(FacebookUserRequest user)
{
FacebookUserModel fbUser = new FacebookUserModel();
// Build FacebookUser object
try {
// Grab basic user details
string profileRequestUri = "https://graph.facebook.com/" + user.fbID + "?access_token=" + user.access_token;
HttpWebRequest profileRequest = (HttpWebRequest)WebRequest.Create(profileRequestUri);
profileRequest.Method = WebRequestMethods.Http.Get;
profileRequest.Accept = "application/json";
HttpWebResponse profileResponse = (HttpWebResponse)profileRequest.GetResponse();
Stream profileResponseStream = profileResponse.GetResponseStream();
StreamReader profileStreamReader = new StreamReader(profileResponseStream);
fbUser = JsonConvert.DeserializeObject<FacebookUserModel>(profileStreamReader.ReadToEnd());
} catch (Exception) ...
try {
// Grab profile picture
string pictureRequestUri = "https://graph.facebook.com/" + user.fbID + "/picture";
HttpWebRequest pictureRequest = (HttpWebRequest)WebRequest.Create(pictureRequestUri);
pictureRequest.Method = WebRequestMethods.Http.Get;
HttpWebResponse pictureResponse = (HttpWebResponse)pictureRequest.GetResponse();
fbUser.profilePictureUri = pictureResponse.ResponseUri.ToString();
} catch (Exception) ...
// If user exists, change password to new token and return)
if(userExists)
{
try {
IdentityUser identityUser = _repo.FindUser(ID, pass).Result;
FacebookUserModel dbUser = db.FacebookUserObjects.First(u => u.identityUserID == identityUser.Id);
db.Entry(dbUser).CurrentValues.SetValues(fbUser);
db.SaveChangesAsync();
fbUser.identityUserID = identityUser.Id;
return fbUser;
}
catch (Exception e)
{ return null; }
}
// Else, create the new user using same scheme
else
{
UserModel newUser = new UserModel
{
UserName = ID,
Password = pass,
ConfirmPassword = pass
};
// Create user in Identity & linked Facebook record
createUser(newUser, fbUser);
return fbUser;
}
}
private async void createUser(UserModel newUser, FacebookUserModel fbUser)
{
IdentityResult result = await _repo.RegisterUser(newUser);
var identityUser = await _repo.FindUser(newUser.UserName, newUser.Password);
fbUser.identityUserID = identityUser.Id;
db.FacebookUserObjects.Add(fbUser);
db.SaveChangesAsync();
}
AngularJS calls to my server:
var _login = function (fbID, fbToken) {
$http.post(serviceBase + 'auth/login', { "fbID": fbID, "access_token": fbToken }).then(function (response) {
var data = "grant_type=password&username=" + fbID + "&password=" + pass;
$http.post(serviceBase + 'auth/token', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
.success(function (tokenResponse) {
authServiceFactory.bearerToken = tokenResponse.access_token;
})
.error(function (err) {
console.log("token error:", err);
});
authServiceFactory.userObject = response.data;
window.localStorage['userObject'] = JSON.stringify(authServiceFactory.userObject);
})
};
Why would I get the No 'Access-Control-Allow-Origin' error only on the first call, but not subsequent ones?
Update
I have a workaround in place that works, but I don't really like. The issue only arose when calling a second method from my login controller, so if I moved that code up into the login controller instead of a secondary method it works without the CORS error. This really bothers me though and is inefficient, I'd love to know a better way around it.
if you're working with angularjs you might want to check out satellizer. It makes the auth process really simple and has some awesome built in window popup control.
As far as the Access-Control-Allow-Origin calls it could be happening because you explicitly set headers on the one call and the other ones are falling back to the default http provider? Check out $http and see if providing those defaults might work around it.