Keycloak REST API 403 forbidden - keycloak

I am trying to delete user session using keycloak REST API,
But getting the 403 forbidden Http status code.
I am passing the token and cookie in to the header, please let me know if I missing something.
static void logOut(String userId,KeycloakSecurityContext session){
userId = "a12c13b7-fa2e-412f-ac8e-376fdca16a83";
String url = "http://localhost:8081/auth/admin/realms/TestRealm/users/a12c13b7-fa2e-412f-ac8e-376fdca16a83/logout";
HttpClient httpclient = HttpClients.createDefault();
HttpPost httppost = new HttpPost(url);
HttpResponse response;
try {
httppost.addHeader("Accept", "application/json");
httppost.addHeader("Content-Type","application/json");
httppost.addHeader("Cookie", "JSESSIONID=CABD8A135C74864F0961FA629D6D489B");
httppost.addHeader("Authorization", "Bearer "+session.getTokenString());
response = httpclient.execute(httppost);
HttpEntity entity = response.getEntity();
System.out.println("entity :"+response.getStatusLine());
if (entity != null) {
String responseString = EntityUtils.toString(entity, "UTF-8");
System.out.println("body ....."+responseString);
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

the user you use to access according functions needs according rights on your realm.
For example my 'admin' user needed a CLIENT ROLE "view-users" of CLIENT "realm-management" to be able to get information about users. In your case, when you need to delete a user, you may need a role "manage-users" or may be something more powerful.

Realm management role of manage-users will give you delete permissions.
You can select realm management from the client roles drop down in the role mappings tab.
Manage-users is a powerful role though, it might give more permissions to the end user than you might like. You can upgrade other users role, delete anyone etc. Do test it out according to your requirements.

Related

Automate user creation and deletion through external API requests

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?

AEM 6.3 Cannot create groups with service user

Hoping someone on here can help me out of a conundrum.
We are trying to remove all Admin sessions from our application, but are stuck with a few due to JCR Access Denied exceptions. Specifically, when we try to create AEM groups or users with a service user we get an Access Denied exception. Here is a piece of code written to isolate the problem:
private void testUserCreation2() {
String groupName = "TestingGroup1";
Session session = null;
ResourceResolver resourceResolver = null;
String createdGroupName = null;
try {
Map<String, Object> param = new HashMap<String, Object>();
param.put(ResourceResolverFactory.SUBSERVICE, "userManagementService");
resourceResolver = resourceResolverFactory.getServiceResourceResolver(param);
session = resourceResolver.adaptTo(Session.class);
// Create UserManager Object
final UserManager userManager = AccessControlUtil.getUserManager(session);
// Create a Group
LOGGER.info("Attempting to create group: "+groupName+" with user "+session.getUserID());
if (userManager.getAuthorizable(groupName) == null) {
Group createdGroup = userManager.createGroup(new Principal() {
#Override
public String getName() {
return groupName;
}
}, "/home/groups/testing");
createdGroupName = createdGroup.getPath();
session.save();
LOGGER.info("Group successfully created: "+createdGroupName);
} else {
LOGGER.info("Group already exists");
}
} catch (Exception e) {
LOGGER.error("Error while attempting to create group.",e);
} finally {
if (session != null && session.isLive()) {
session.logout();
}
if (resourceResolver != null)
resourceResolver.close();
}
}
Notice that I'm using a subservice name titled userManagementService, which maps to a user titled fwi-admin-user. Since fwi-admin-user is a service user, I cannot add it to the administrators group (This seems to be a design limitation on AEM). However, I have confirmed that the user has full permissions to the entire repository via the useradmin UI.
Unfortunately, I still get the following error when I invoke this code:
2020-06-22 17:46:56.017 INFO
[za.co.someplace.forms.core.servlets.IntegrationTestServlet]
Attempting to create group: TestingGroup1 with user fwi-admin-user
2020-06-22 17:46:56.025 ERROR
[za.co.someplace.forms.core.servlets.IntegrationTestServlet] Error
while attempting to create group. javax.jcr.AccessDeniedException:
OakAccess0000: Access denied at
org.apache.jackrabbit.oak.api.CommitFailedException.asRepositoryException(CommitFailedException.java:231)
at
org.apache.jackrabbit.oak.api.CommitFailedException.asRepositoryException(CommitFailedException.java:212)
at
org.apache.jackrabbit.oak.jcr.delegate.SessionDelegate.newRepositoryException(SessionDelegate.java:670)
at
org.apache.jackrabbit.oak.jcr.delegate.SessionDelegate.save(SessionDelegate.java:496)
Is this an AEM bug, or am I doing something wrong here?
Thanks in advance
So it seems the bug is actually in the old useradmin interface. It was not allowing me to add my system user into the admninistrators group, but this is possible in the new touch UI admin interface.

How to prevent infinite retries - Apache Shiro RESTful service

Goal
I am setting up a RESTful webservice, using RESTEasy framework. For security I use Apache Shiro. I want my api to stop accepting requests or timing out persons that login too much.
Problem
Whenever I go some URL with my browser (chrome), I can try to login infinitely many times. Seems a really bad idea to allow this. As a measure, I have made sure to remember the nr of login attempts, for which users cannot login after 3 times. However, with a brute force attack, you could still block all users from loging in. I want a more general solution.
Shiro.ini
[main]
# We store users and passwords inside the realm.
myRealm = com.myproject.shiro.DatabaseRealm
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
securityManager.sessionManager = $sessionManager
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $cacheManager
[urls]
/api/version = anon
/api/** = authcBasic
DatabaseRealm
public class DatabaseRealm extends AuthorizingRealm {
#Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// No clue what to do with this functin. I only use authentication and not authorization, so probably just nothing.
return null;
}
/**
* Check if the user inputted is valid. The user can login if holds:
* 1. Password is correct. (if not, nrOfLogonAttempts++)
* 2. LogonUser.nrOfLogonAttemps is less than 3
* 3. LogonUser.dateEndValid is null or >= today.
* #param authenticationToken Token with basic information.
* #return SimpleAuthenticationInfo
* #throws AuthenticationException Whenever the user cannot login.
*/
#SuppressWarnings("ConstantConditions")
#Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws UnknownAccountException, IncorrectCredentialsException, LockedAccountException, ExpiredCredentialsException {
// Connect with the database.
DbContext context = null;
try {
context = DbContextUtil.getContextFromTomcat();
// Lookup user in the database.
LogonUserMyDao logonUserMyDao = new LogonUserMyDao(context);
LogonuserPojo logonuserPojo = logonUserMyDao.fetchOneByUsername(((UsernamePasswordToken) authenticationToken).getUsername());
if (logonuserPojo == null) {
throw new UnknownAccountException("Could not find user.");
}
// Check password
String plainTextPassword = new String(((UsernamePasswordToken) authenticationToken).getPassword());
if (!BCryptUtil.checkPassword(plainTextPassword, logonuserPojo.getPassword())) {
// We will note this event.
logonuserPojo.setNroflogonattempts(logonuserPojo.getNroflogonattempts() + 1);
logonUserMyDao.update(logonuserPojo);
context.commit();
throw new IncorrectCredentialsException("Incorrect password.");
}
// Check nrOfLogonAttempts
if (logonuserPojo.getNroflogonattempts() >= 2) {
throw new LockedAccountException("Cannot login anymore.");
}
// Check date
if (logonuserPojo.getDateendvalid() != null && DateTimeUtil.isBeforeToday(logonuserPojo.getDateendvalid())) {
throw new ExpiredCredentialsException("Account is expired.");
}
// User is valid, so return some info.
return new SimpleAuthenticationInfo(logonuserPojo.getUsername(), plainTextPassword, getClass().getName());
} catch (SQLException e) {
MyLogger.logError("Could not connect to user database.", e);
throw new AuthenticationException("Could not connect to databse.");
} finally {
if (context != null) {
try {
context.getConnection().close();
} catch (SQLException e) {
MyLogger.logError("Could not close connection", e);
}
}
}
}
}
Are you looking for more general DDOS protection? There are a few options out there depending on where your app is running (for example AWS Shield).
You could also prevent connections from reaching your db with something like this: https://github.com/iTransformers/ddos-servlet-filter (but, that that would still require handling the request in your application)
On the Shiro side of things, counting your attempts is NOT a bad idea, but you need to watch out for the user management side of things (How does a user get unlocked, support request? Wait 30 minutes?) Instead of recording failures, you may just want to record/audit all attempts (excluding the actual password of course). With either option a call to support or an n minute window, this may help provide some context to support or an easy query.

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.

Cloud SQL Admin API

I've been working with sqladmin-appengine-sample and the v1beta3 json API.
The Java code is running on App Engine. oauth2.
I can get it to work where when the currently logged in user is the app owner, but what I think I need is something like AppIdentityCredential so that the app can access any of the SQL instances it has access to regardless of the currently logged in user.
How do I do this?
Do I need to use a service account?
The short answer is that I could not get AppIdentityCredential to work, but setting up a Service Account credential did work. Here is the code:
Set<String> oAuthScopes = new HashSet<String>();
oAuthScopes.add(SQLAdminScopes.CLOUD_PLATFORM);
oAuthScopes.add(SQLAdminScopes.SQLSERVICE_ADMIN);
// service account credential
GoogleCredential credential;
try {
File p12File = new File(servletContext.getResource(PK12_FILE_NAME).toURI());
credential = new GoogleCredential.Builder()
.setTransport(Utils.HTTP_TRANSPORT)
.setJsonFactory(Utils.JSON_FACTORY)
.setServiceAccountId(SERVICE_ACCOUNT_ID)
.setServiceAccountScopes(oAuthScopes)
.setServiceAccountPrivateKeyFromP12File(p12File)
.build();
} catch (Exception e) {
throw new SecurityException(e);
}
// build the SQLAdmin object using the credentials
this.sqlAdmin = new SQLAdmin.Builder(Utils.HTTP_TRANSPORT, Utils.JSON_FACTORY, credential)
.setApplicationName(APPLICATION_NAME)
.build();
String timestamp = new Date().toString().replace(" ", "_").replace(":", "_");
ExportContext exportContent = new ExportContext();
exportContent.setDatabase(Arrays.asList(database_name));
exportContent.setKind("sql#exportContext");
exportContent.setUri("gs://"+GCS_BUCKET_NAME+"/"+database_name+"_"+timestamp+".mysql");
InstancesExportRequest exportRequest = new InstancesExportRequest();
exportRequest.setExportContext(exportContent);
// execute the exportRequest
this.sqlAdmin.instances().export(APPLICATION_NAME, instance_name, exportRequest).execute();