I have a Webapi service that generates the password Reset token.
Token generation service end point:
[Authorize(Users = "abcd")]
[HttpGet]
[ActionName("GenerateForgotPasswordToken")]
public async Task<IHttpActionResult> GenerateForgotPasswordToken(string key)
{
if (key == null || UserManager.FindById(key) == null)
{
return InternalServerError(new Exception("User not found"));
}
return Ok(await UserManager.GeneratePasswordResetTokenAsync(key));
}
My application UserManager:
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
//var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
var manager = new ApplicationUserManager(new UserStore<IdentityUser>(new MTA()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<IdentityUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<IdentityUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
The token will be used in an email to send a password reset URL to the user. That URL points to an ASP.NET MVC view which is part of my WebAPI project and obviously is hosted in the same web application in IIS. The password reset button on the page calls my other service endpoint which resets the password.
Passowrd reset service end point:
[HttpGet]
[AllowAnonymous]
public async Task<HttpResponseMessage> ResetPassword([FromUri]string email,[FromUri]string code,[FromUri]string password)
{
var user = await UserManager.FindByEmailAsync(email);
var result = await UserManager.ResetPasswordAsync(user.Id, code, password);
if (result.Succeeded)
{
return Request.CreateResponse();
}
return Request.CreateResponse(System.Net.HttpStatusCode.Ambiguous);
}
Also it might help to mention that both those web api endpoints are in the same controller and in the controller I defined a global UserManger as follows:
private ApplicationUserManager _userManager;
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
When I use an external tool like Advanced REST Client, I am able to hit the first endpoint to generate the token and then pass the token to the second endpoint along with email and new password and successfully reset the password. However when my ASP.NET MVC controller uses the same token generated by the first endpoint and call the passwordReset endpoint , the token is invalid! I already make sure that the is no Encoding/Decoding issue and the token that is received by the second end point is identical in both tests.
Once again all my WebApi and ASP controllers are in the same project and hosted in the same web application.
I think the problem might be related to having a new Token provider when a request is comming based on the OwinContext but I don't understand why it works calling it through web browser.
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
I found the problem after reading this question:
Make ASP.NET Identity 2.0 Email confirm token work for WCF and MVC
In my case IIS was configured to generate the Machine Key at runtime and that's why the token was invalid even when all my code was running as a single application.
Here is a guide to IIS machine key configuration :How to Generate Machine Key using IIS
Related
I'm trying to find a proper way where I can inject a service to validate if user exists or registered in my application after being successfully authenticated from an external identity provider like Azure Active Directory. What I want to do is to redirect user to a custom error page or display an Unauthorized message if his account is not yet registered in my application.
I tried utilizing the IProfileService interface but it seems not the right way to go.
Here is my Startup.cs setup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddTestUsers(Config.GetUsers())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients()) // Client was configured with RequireConsent = false, EnableLocalLogin = false,
.AddProfileService<ProfileService>()
.Services.AddTransient<IUserRepository,UserRepository>();
services.AddAuthentication()
.AddOpenIdConnect("AAD", "Azure Active Directory", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://login.microsoftonline.com/MyTenant";
options.ClientId = "MyClientId";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
options.GetClaimsFromUserInfoEndpoint = true;
});
}
public class ProfileService : IProfileService
{
private readonly IUserRepository _userRepository;
public ProfileService(IUserRepository userRepository)
{
_userRepository = userRepository
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = _userRepository.FindByUser(context.Subject.Identity.Name);
// This will display HTTP 500 instead of 401
if(user == null) throw new UnauthorizedAccessException("You're not registered");
// I add custom claims here
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context) => Task.FromResult(0);
}
Is there any available service or interface I can use where I can inject my user validation as well as allowing me to inject my user repository in that service? Is it possible to inject this kind of process inside IdentityServer4? Can someone point me in the right direction to accomplish my goal using IdentityServer4?
Note: Lets assume I have SPA web app and I have my own separate registration mechanism. I don't want to redirect back to my SPA if user doesn't exist and handle it inside IdentityServer4 instead. Btw, some of the code above are not included for brevity.
The IdentityServer4 QuickStart UI is configured to auto-provision local user accounts when signing-in through an external provider. That's all handled in ExternalController.Callback:
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = AutoProvisionUser(provider, providerUserId, claims);
}
In your situation, you can perform whatever logic you need to perform instead of calling AutoProvisionUser. As this is a regular MVC action that's being executed, you have the ability to inject your own classes into ExternalController's constructor or into Callback itself (using [FromServices]). Here's a rough idea of the changes you might want to make:
public async Task<IActionResult> Callback([FromServices] IUserRepository userRepository)
{
...
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
if (user == null)
{
// We don't have a local user.
return RedirectToAction("SomeAction", "SomeController");
}
...
}
You can write your custom logic in ExternalLoginCallback function in in AccountController if you are using ASP.NET Identity . After getting JWT token issued from Azure AD , you can decode the token ,get the user claims such as email/name :
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
// read external identity from the temporary cookie
var aadResult1 = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (aadResult1?.Succeeded != true)
{
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var externalUser = aadResult1.Principal;
if (externalUser == null)
{
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var claims = externalUser.Claims.ToList();
// try to determine the unique id of the external user - the most common claim type for that are the sub claim and the NameIdentifier
// depending on the external provider, some other claim type might be used
var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
if (userIdClaim == null)
{
userIdClaim = claims.FirstOrDefault(x => x.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier");
}
if (userIdClaim == null)
{
throw new Exception("Unknown userid");
}
Then you can write your service implement/logic in database to confirm whether user is already in database . If yes , login in user;if no , redirect user to confirmation/register view . Something like:
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync("YourProvider", userIdClaim.Value, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in with {Name} provider.", "YourProvider");
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToAction(nameof(Lockout));
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = "YourProvider";
var email = claims.FirstOrDefault(x => x.Type == ClaimTypes.Upn).Value;
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}
It depends on you for how to link AD user to local database user .use Azure AD's object ID or UPN .
I'm using IdentityServer4 and have configured an OpenId Connect provider. What I want to do is pass in a username to the provider as part of the querystring so that the provider pre-fills in the username field. I have both ADFS and Azure AD providers and would like this functionality to work with both. Is this possible and if so how?
In the Challenge method on ExternalController I've added what I think should work but it doesn't do anything:
[HttpGet]
public async Task<IActionResult> Challenge(string provider, string returnUrl, string user)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
{
throw new Exception("invalid return URL");
}
if (AccountOptions.WindowsAuthenticationSchemeName == provider)
{
return await ProcessWindowsLoginAsync(returnUrl);
}
else
{
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", provider },
{ "login_hint", user }
}
};
return Challenge(props, provider);
}
}
You can achieve what you're looking for using the OnRedirectToIdentityProvider property of the OpenIdConnectEvents class:
Invoked before redirecting to the identity provider to authenticate. This can be used to set ProtocolMessage.State that will be persisted through the authentication process. The ProtocolMessage can also be used to add or customize parameters sent to the identity provider.
You hook into this process via the AddOpenIdConnect function, which is called when using services.AddAuthentication in Startup.ConfigureServices. Here's an example of what this might look like for your requirements:
services
.AddAuthentication(...)
.AddOpenIdConnect(options =>
{
...
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = ctx =>
{
if (ctx.HttpContext.Request.Query.TryGetValue("user", out var stringValues))
ctx.ProtocolMessage.LoginHint = stringValues[0];
return Task.CompletedTask;
}
};
});
Most of this is just the boilerplate code for adding authentication, OIDC and registering an event-handler for the event detailed above. The most interesting part is this:
if (ctx.HttpContext.Request.Query.TryGetValue("user", out var stringValues))
ctx.ProtocolMessage.LoginHint = stringValues[0];
As your Challenge action from your question gets user from a query-string parameter, the code above reads out the user query-string parameter from the request (there could be more than one, which is why we have a StringValues here) and sets it as the LoginHint property, if it's found.
Note: I've tested this with https://demo.identityserver.io (which works, of course).
I'm securing a Web API site, and I want to use tokens. But, I'm working with a legacy database, where there is a users table and each user already has a token created for them and stored in the table.
I'm trying to work out if I can use the Identity oAuth bearer token auth bits, but plug it all into my existing database, so that
Granting a token just returns the token for that user from the db
I can validate the token by looking it up in the db and creating an identity from the user (I am using ASP.NET Identity elsewhere in the site for the MVC side of things)
I can't work out if this is going to be possible, or if I should give up and use a standard HTTP handler approach. Here's my fairly standard code so far, which just issues standard tokens, not the existing ones I want to work with.
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider()
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
var bearerAuth = new OAuthBearerAuthenticationOptions()
{
Provider = new OAuthBearerAuthenticationProvider()
};
app.UseOAuthBearerAuthentication(bearerAuth);
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
var manager = new UserManager<User, long>(new UserStore(new UserRepository()));
var user = await manager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
}
else
{
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("name",user.Email));
context.Validated(identity);
}
}
}
Answering my own question ;)
Yes, it is possible. It mostly requires that you sort out a custom Token provider and implement your logic in there. A good sample of this:
https://github.com/eashi/Samples/blob/master/OAuthSample/OAuthSample/App_Start/Startup.Auth.cs
Following this guide for external auth using MVC 5 on Owin - External login providers with owinkatana.
I have added the following to my Owin Nancy application
Startup.cs -
app.Properties["Microsoft.Owin.Security.Constants.DefaultSignInAsAuthenticationType"] = "ExternalCookie";
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ExternalCookie",
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
});
app.UseTwitterAuthentication(new TwitterAuthenticationOptions
{
ConsumerKey = "mykey",
ConsumerSecret = "mypass"
});
LoginModule.cs (nancy module)
Post["ExternalLogin"] = _ =>
{
var provider = Request.Form.name;
var auth = Context.GetAuthenticationManager();
auth.Challenge(new AuthenticationProperties
{
RedirectUri = String.Format("/?provder={0}", provider)
}, provider);
return HttpStatusCode.Unauthorized;
};
Now at the challenge point here nothing happens whatsoever. It just shows a blank page with the Url of the redirect. I have confirmed that I can get it to work following the example in MVC.
Does anyone know the correct Nancy code for this section?
I'll expand on a comment I was about to leave and just make it an answer (even though you moved away from Nancy it seems). I asked a similar question, and was pointed to the following code example on github:
https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/tree/dev/samples/Nancy/Nancy.Client
Assuming you have your OIDC wired up properly in Startup.cs, the following code is what I needed to get Nancy module to trigger the authentication on my signin/signout routes:
namespace Nancy.Client.Modules {
public class AuthenticationModule : NancyModule {
public AuthenticationModule() {
Get["/signin"] = parameters => {
var manager = Context.GetAuthenticationManager();
if (manager == null) {
throw new NotSupportedException("An OWIN authentication manager cannot be extracted from NancyContext");
}
var properties = new AuthenticationProperties {
RedirectUri = "/"
};
// Instruct the OIDC client middleware to redirect the user agent to the identity provider.
// Note: the authenticationType parameter must match the value configured in Startup.cs
manager.Challenge(properties, OpenIdConnectAuthenticationDefaults.AuthenticationType);
return HttpStatusCode.Unauthorized;
};
Get["/signout"] = Post["/signout"] = parameters => {
var manager = Context.GetAuthenticationManager();
if (manager == null) {
throw new NotSupportedException("An OWIN authentication manager cannot be extracted from NancyContext");
}
// Instruct the cookies middleware to delete the local cookie created when the user agent
// is redirected from the identity provider after a successful authorization flow.
manager.SignOut("ClientCookie");
// Instruct the OpenID Connect middleware to redirect
// the user agent to the identity provider to sign out.
manager.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationType);
return HttpStatusCode.OK;
};
}
}
}
Code source: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/dev/samples/Nancy/Nancy.Client/Modules/AuthenticationModule.cs
Hope that helps!
I have an asp.net mvc4 web application that is consuming data data from an API written in C# and hosted on a Linux machine w/ Apache / mod_mono
The client application is written in C#/asp.net - It runs on a different web server, also Linux / Apache / mod_mono. I'm not sure if those details are important in this case, but I figured any background may help.
The question leading up to this one: AppHostBase instance not set - Helped me gain quite a bit more understanding of how this all fits together.
I believe the proper question I should be asking now is: Once I create a session in servicestack (On the API server), how do I properly reconnect to it?
Following the answers in previous questions, I've used this bit of code in my auth controller on the client application:
var authService = AppHostBase.Resolve<AuthService>();
authService.RequestContext = System.Web.HttpContext.Current.ToRequestContext();
var AuthResponse = authService.Authenticate(new Auth
{
provider = "credentials",
UserName = user.user_id,
Password = user.password,
RememberMe = true
});
This returns a ResolutionException:
Required dependency of type ServiceStack.ServiceInterface.Auth.AuthService could not be resolved.
Is there something simple I might be missing when it comes to getting the client to work from within an asp.net application?
I apologize if the question is too vague and will happily provide any more information.
Update:
This is AuthController - Excuse the mess, I've been trying a few things since my last post:
{
public partial class AuthController : BaseController
{
JsonServiceClient client = new ServiceStack.ServiceClient.Web.JsonServiceClient("<TheAPIurl>");
// GET: /Login/
public ActionResult login()
{
if (Session["IsAuthenticated"] != null)
{
ViewData["Result"] = Session["IsAuthenticated"];
}
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult login(UserModel user)
{
try
{
var authService = AppHostBase.Resolve<AuthService>();
authService.RequestContext = System.Web.HttpContext.Current.ToRequestContext();
var AuthResponse = authService.Authenticate(new Auth
{
provider = "credentials",
UserName = user.user_id,
Password = user.password,
RememberMe = true
});
if (AuthResponse.SessionId != null)
{
Session["IsAuthenticated"] = true;
Session["UserID"] = AuthResponse.UserName;
Session["jsclient"] = client;
FormsAuthentication.SetAuthCookie(user.user_id, true);
return Redirect("/default");
}
else
{
Session["IsAuthenticated"] = false;
}
}
catch (Exception ex)
{
Session["IsAuthenticated"] = false;
}
return View();
}
protected override void ExecuteCore()
{
throw new NotImplementedException();
}
}
}
Authenticating with a local ServiceStack instance
You can only retrieve an auto-wired ServiceStack service (or other IOC dependency) out from a ServiceStack Container if the ServiceStack instance is hosted within the same App Domain as MVC, i.e:
var authService = AppHostBase.Resolve<AuthService>();
authService.RequestContext = System.Web.HttpContext.Current.ToRequestContext();
Although the recommended code for resolving the auto-wired implementation of another service is:
using (var authAservice = AppHostBase.ResolveService<AuthService>()) {
...
}
i.e. As services may make use of resources that should be disposed. Inside a ServiceStack service you should use base.ResolveService<AuthService>() instead.
So if ServiceStack hosted within the same AppDomain as MVC, you can call the Service directory, like this:
var authResponse = authService.Authenticate(new Auth {
provider = "credentials",
UserName = user.user_id,
Password = user.password,
RememberMe = true
});
Authenticating with a Remote ServiceStack instance
Otherwise if it's remote you need to use one of ServiceStack's C# Service Clients, e.g:
var client = new JsonServiceClient(ServiceStackBaseUrl);
var authResponse = client.Post(new Auth {
provider = "credentials",
UserName = user.user_id,
Password = user.password,
RememberMe = true
});
Attaching ServiceStack SessionId back to originating MVC request
This will setup an authenticated session with that ServiceClient client, by attaching it to the ss-pid Cookie (see Session docs for more info). You can pass through this cookie to the originating browser that called MVC with:
var response = HttpContext.Current.Response.ToResponse();
response.Cookies.AddSessionCookie(
SessionFeature.PermanentSessionId, authResponse.SessionId);
Subsequent requests with the authenticated session
To re-attach with the remote authenticated ServiceStack Session from within MVC you would then need to pass the cookie back into the Service Client, e.g:
var cookie = HttpContext.Request.Cookies.Get(SessionFeature.PermanentSessionId);
var client = new JsonServiceClient(ServiceStackBaseUrl);
var cookie = new Cookie(SessionFeature.PermanentSessionId, cookie.Value);
client.CookieContainer.Add(cookie);
You can set the cookie domain, globally in the Web.Config:
<httpCookies domain="mydomain.com" />
Or at runtime with:
cookie.Domain = "mydomain.com";
The ServiceStack AuthTests.cs Integration Tests has some other useful examples showing how Authentication works in ServiceStack.