Reputation: 400
I want to use IdentityServer4 to implement OIDC authentication for future development. We have an existing homegrown membership schema that was implemented before Asp.Net membership or Aspnet Identity. We have legacy applications using the existing membership schema to authenticate and authorize users.
How can I implement an adapter that points to the legacy schema, (assume a Users table with username & password columns), for the user store? The only examples I've seen are InMemory users and AspnetIdentity.
Upvotes: 4
Views: 1649
Reputation: 433
I have a similar implementation requirements but wont it be more complaint to use the acr_values header with tenant:company1 as the company identifier ?
Upvotes: 0
Reputation: 400
The legacy system I needed to adapt to IdentityServer4 requires three pieces of information to authentication a user: company identifier, username, and password.
Our system has a master database with a Companies table that contains connection strings to company-specific databases (tenants). Each tenant db has a Users table with user profile information including Username and Password.
Using Scott Brady's answer I was able to implement an IResourceOwnerPasswordValidator
that worked for our situation, but not without digging through the IdentityModel.TokenClient sourcecode. What I discovered is that the token endpoint (token_endpoint: "http:///connect/token") takes a URL encoded form and serializes the key/value pairs and makes them available in IResourceOwnerPasswordValidator.ValidateAsync
via the Request.Raw
property of the ResourceOwnerPasswordValidationContext
argument.
Here's an example of my ResourceOwnerPasswordValidator.ValidateAsync()
implementation showing how I authenticate with additional parameters beyond username and password:
public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
_logger.LogInformation("Begin resource owner password validation.");
var username = context.UserName;
var password = context.Password;
var company = context.Request.Raw.Get("company");
if (string.IsNullOrWhiteSpace(company))
{
_logger.LogError("'company' doesn't exist in ResourceOwnerPasswordValidationContext.Request.Raw collection.");
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Company is required.");
return Task.FromResult(0);
}
if (string.IsNullOrWhiteSpace(username))
{
_logger.LogError("'username' is null or whitespace.");
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Username is required.");
return Task.FromResult(0);
}
var userRepo = _userRepositoryFactory.GetUserRepositoryForCompany(company);
var user = userRepo.GetUserByUserId(username);
if (user == null)
{
_logger.LogError($"No user found for company {company} and username {username}.");
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, $"No user {username} found for for {company}.");
return Task.FromResult(0);
}
_logger.LogInformation("Resource owner password validation succeeded.");
context.Result = user.Password == password ? new GrantValidationResult(context.UserName, GrantType.ResourceOwnerPassword, new [] { new Claim("company", company) }) : new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Invalid username or password.");
return Task.FromResult(0);
}
I used Postman to verify my results by hitting the token endpoint. First, I configured a client in my IdentityServer4 implementation:
new Client {
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email
},
ClientId = "test.client",
ClientName = "Test Client",
ClientSecrets = new List<Secret>
{
new Secret("secret".Sha256())
}
}
In Postman I selected Basic Auth for the Authorization Type. The Username is the ClientId and the Password is the Client Secret. I set the verb to POST and specified a "x-www-form-urlencoded" body. The endpoint requires minimum values for username, password, grant_type, and scope. I added a company key/value argument:
Upvotes: 3
Reputation: 5598
To integrate with your own user store you need to create an implementation of IProfileService
(and optionally IResourceOwnerPasswordValidator
if you need to use the resource owner grant type), and then register it in IServiceCollection
.
e.g: services.AddTransient<IProfileService, GabeFcCustomProfileService>();
Upvotes: 2