I'm using Keycloak inside docer and hosted on https://accounts.example.com.
I'm using React as my client library and after login, user goes into https://user.example.com.
This React client uses .NET Core API located at https://api.user.example.com.
I use keycloak.js to manage access in React panel.
Now let's say user wants to create a ticket. I send this ticket info to the API:
{
title: 'Urgent backup',
priority: 'urgent',
description: 'Please backup my files immediately'
}
In our database, we have this design:
create table Tickets
(
Id bigint not null primary identity(1, 1),
UserGuid uniqueidentifier not null, -- How should I fill this column?
Title nvarchar(200) not null,
PriorityId int not null,
Description nvarchar(max) not null
)
The problem is, API should extract a permanent data from user.
I can't store email or username in that column of course. And I can't even rely on username or email because Keycloak's admin panel allows this setting to be configured:
Thus the best option is to give user a GUID or UUID in Keycloak, and retrieve it in open id token.
I searched but I can't find how to do this.
Can you help?
By default your acccess token should have a subject (sub) claim with a value of the user's id in Keycloak. That is a UUID.
You can extract that ID from the token and use it.
Maybe ASP.NET core will do this automatically and provide the subject claim as the username for you.
I am not sure where exactly is the issue ,Keycloak already provide the API to get/update/delete user
Get users Returns a list of users, filtered according to query parameters:
GET /{realm}/users
In Query Parameters you can filter according to email/first/lastName/username
and it will give result something like this if you want to fetch all user
[
{
"id":"569f67de-36e6-4552-ac54-e52085109818",
"username":"user1",
"enabled":true,
...
},
{
"id":"90afb701-fae5-40b4-8895-e387ba34902jh",
"username":"user2",
"enabled":true,
....
}
]
If you want to fetch individual user ,you can get it that also with below API
GET /{realm}/users/{id}
If you see the first rest query ,you can query as per your requirement with Query Parameters and get the result and then id can be use for your second table.
Edit -
By default, the User ID can be obtained directly from the principal
String userId = accessToken.getSubject();
in .Net Core I found this
public static T GetLoggedInUserId<T>(this ClaimsPrincipal principal)
{
if (principal == null)
throw new ArgumentNullException(nameof(principal));
var loggedInUserId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (typeof(T) == typeof(string))
{
return (T)Convert.ChangeType(loggedInUserId, typeof(T));
}
else if (typeof(T) == typeof(int) || typeof(T) == typeof(long))
{
return loggedInUserId != null ? (T)Convert.ChangeType(loggedInUserId, typeof(T)) : (T)Convert.ChangeType(0, typeof(T));
}
else
{
throw new Exception("Invalid type provided");
}
}
Related
I am currently setting up a new project using Laravel 8. Out of the box, Laravel is configured to use auto-incrementing ID's for the user's ID. In the past I have overrode this by doing the following.
Updating the ID column in the user table creation migration to
$table->uuid('id');
$table->primary('id');
Adding the following trait
trait UsesUUID
{
protected static function bootUsesUUID()
{
static::creating(function ($model) {
$model->{$model->getKeyName()} = (string) Str::orderedUuid();
});
}
}
Adding the following to the user model file
use UsesUUID;
public $incrementing = false;
protected $primaryKey = 'id';
protected $keyType = 'uuid';
On this new project, I did the same as above. This seems to break the login functionality. When the email and password are entered and submitted, the form clears as though the page has been refreshed. Thing to note is there are no typical validation error messages returned as would be expected if the email and/or password is wrong.
To check that the right account is actually being found and the password is being checked properly, I added the following code to the FortifyServiceProvider boot method. The log file confirms that the user is found and the user object dump is correct too.
Fortify::authenticateUsing(function(Request $request) {
\Log::debug('running login flow...');
$user = User::where('email', $request->email)->first();
if ($user && Hash::check($request->password, $user->password)) {
\Log::debug('user found');
\Log::debug($user);
return $user;
}
\Log::debug('user not found');
return false;
});
Undoing the above changes to the user model fixes the login problem. However, it introduces a new problem that is the login will be successful but it wont be the right account that is logged in. For example, there are 3 accounts, I enter the credentials for the second or third account, but no matter what, the system will always login using the first account.
Anyone have any suggestions or ideas as to what I may be doing wrong, or if anyone has come across the same/similar issue and how you went about resolving it?
Thanks.
After digging around some more, I have found the solution.
Laravel 8 now stores sessions inside the sessions table in the database. The sessions table has got a user_id column that is a foreign key to the id column in the users table.
Looking at the migration file for the sessions table, I found that I had forgot to change the following the problem.
From
$table->foreignId('user_id')->nullable()->index();
To
$table->foreignUuid('user_id')->nullable()->index();
This is because Laravel 8 by default uses auto incrementing ID for user ID. Since I had modified the ID column to the users table to UUID, I had forgotten to update the reference in the sessions table too.
I using ASP.Net Core 3 Identity with Identity Server 4 for authentication ...
On the AspNetIdentity template the External Authentication Controller Callback method calls the AutoProvisionUserAsync method which has the following code:
var email = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Email)?.Value ??
claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value;
if (email != null) {
filtered.Add(new Claim(JwtClaimTypes.Email, email));
}
var user = new User {
UserName = Guid.NewGuid().ToString(),
};
var identityResult = await _userManager.CreateAsync(user);
Basically it creates a user with a Guid as Username ...
In my database I am using Email as Username ... Is there any reason to use a Guid?
I suppose most External authentication services (Google, Facebook, etc) provides an Email.
So my idea would be:
Check if there is an User in the database already with that email.
If no User exists create one with the email obtained from the External authentication service.
Also add the external authentication to the User in the database;
If there is a User with the email in the database check if it has that External Login.
If the user does not have the external login register and add it.
Does this make sense?
Check if there is an User in the database already with that email.
On callback, first call is to FindUserFromExternalProviderAsync, it search users using nameIdentifier, then if not found there is call to AutoProvisionUserAsync
Basically it creates a user with a Guid as Username ...
In my database I am using Email as Username ... Is there any reason to use a Guid?
The ApplicationUser's base class is IdentityUser, IdentityUser has a prop for ID and one for email by design. thats why most of libraries take advantage of having GUID as ID in addition of email for extensibility. You can use the email for ID if you like to.
I created a delegation extension grant the way they did in the docs. (https://identityserver4.readthedocs.io/en/latest/topics/extension_grants.html)
In the example, they get the user's identity from the claims and return the grant validation result like so:
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
context.Result = new GrantValidationResult(sub, GrantType);
My issue is that I don't always have a subject aka user identity when I need to utilize the delegation grant. In my scenario, I have an application listening to messages. When the app gets a message, it calls an API using client_credentials. That API then calls a sub API using the delegation grant type. Since the app is using client_credentials, there is no "sub" in the claims.
I tried checking if the "sub" claim exists and if not, set the subject of the GrantValidationResult to a "magical" guid which the IUserStore's FindByIdAsync would look for and either return null or a newed up empty TUser. In both cases, this causes Microsoft.AspNetCore.Identity to bomb futher down the pipeline.
How can I return a GrantValidationResult with the current claims, but not the subject when it doesn't exist?
I found this override for the GrantValidationResult.
// Summary:
// Initializes a new instance of the IdentityServer4.Validation.GrantValidationResult
// class with no subject. Warning: the resulting access token will only contain
// the client identity.
public GrantValidationResult(Dictionary<string, object> customResponse = null);
Since I don't have any custom responses, if "sub" is null, then I do this:
context.Result = new GrantValidationResult(new Dictionary<string, object>());
Doing it this way still populates the claims with the requested/validated scopes.
I have hunted through Firebase's docs and can't seem to find a way to add custom attributes to FIRAuth. I am migrating an app from Parse-Server and I know that I could set a user's username, email, and objectId. No I see that I have the option for email, displayName, and photoURL. I want to be able to add custom attributes like the user's name. For example, I can use:
let user = FIRAuth.auth()?.currentUser
if let user = user {
let changeRequest = user.profileChangeRequest()
changeRequest.displayName = "Jane Q. User"
changeRequest.photoURL =
NSURL(string: "https://example.com/jane-q-user/profile.jpg")
changeRequest.setValue("Test1Name", forKey: "usersName")
changeRequest.commitChangesWithCompletion { error in
if error != nil {
print("\(error!.code): \(error!.localizedDescription)")
} else {
print("User's Display Name: \(user.displayName!)")
print("User's Name: \(user.valueForKey("name"))")
}
}
}
When I run the code, I get an error that "usersName" is not key value compliant. Is this not the right code to use. I can't seem to find another way.
You can't add custom attributes to Firebase Auth. Default attributes have been made available to facilitate access to user information, especially when using a provider (such as Facebook).
If you need to store more information about a user, use the Firebase realtime database. I recommend having a "Users" parent, that will hold all the User children. Also, have a userId key or an email key in order to identify the users and associate them with their respective accounts.
Hope this helps.
While in most cases you cannot add custom information to a user, there are cases where you can.
If you are creating or modifying users using the Admin SDK, you may create custom claims. These custom claims can be used within your client by accessing attributes of the claims object.
Swift code from the Firebase documentation:
user.getIDTokenResult(completion: { (result, error) in
guard let admin = result?.claims?["admin"] as? NSNumber else {
// Show regular user UI.
showRegularUI()
return
}
if admin.boolValue {
// Show admin UI.
showAdminUI()
} else {
// Show regular user UI.
showRegularUI()
}
})
Node.js code for adding the claim:
// Set admin privilege on the user corresponding to uid.
admin.auth().setCustomUserClaims(uid, {admin: true}).then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
I have created a visualforce page for creating new user and through the controller class I am trying to insert the newly created user.
I am working on a developer org and has three licenses available for - Salesforce Platform - User License.
I have created 4 users with this license in my org and only one of them is active.
Still I am facing the License Limit Exceeded Exception while I am trying to insert a new user with this license.
Can anyone please suggest a solution for this issue?
The code I am using is -
newUser = new User();
newUser.email = ApexPages.currentPage().getParameters().get('email');
String[] name = ApexPages.currentPage().getParameters().get('name').split(' ',2);
newUser.firstName = name[0];
newUser.lastName = name[1];
newUser.userName = newUser.email;
UserRole usrRole = [select id, name from UserRole where name = 'Customer'];
Profile prof = [select id, name from Profile where name = 'Customer Portal'];
newUser.UserRoleId = usrRole.id;
newUser.ProfileId = prof.id;
newUser.isActive = true;
try
{
insert newUser;
alertMsg = 'Successfully Created New User';
}
catch(System.DMLException e)
{
alertMsg = e.getMessage();
}
The screen shot of my company information is -
Option 1:
In the developer console, run this anonymous apex code snippet to get a count of active and inactive users:
system.debug([select isactive, count(Id) from User group by isactive]);
The outcome of the above SQL should be 3 inactive users and 1 active (as per your problem statement).
Option 2:
Make sure your controller class is not bulk inserting users beyond the # of open licenses.
Option 3:
Make sure your code is not trying to create user(s) using a profile that is tied to a license type that is all used up.
In addition to setting the profile, you also need to set the UserType (aka license type), otherwise it defaults to the Salesforce license, for which you have no spares, you need to add
something like newUser.userType = 'Platform' (you'll need to use describeSObject or a schema viewer tool to find the exact picklist value to use)