I've got an app that has been running using CredentialsAuthProvider() for a while.
Today, I'd like to add twitter and facebook as options for people to create an account. I've got the scaffolding in place.
//Register all Authentication methods you want to enable for this web app.
Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(),
new TwitterAuthProvider(appSettings),
new FacebookAuthProvider(appSettings)
}));
//Provide service for new users to register so they can login with supplied credentials.
Plugins.Add(new CustomRegistrationFeature());
The endpoints /api/auth/twitter and /api/auth/facebook work, and I see that a user session is created by visiting /api/auth once the OAuth process is complete HOWEVER...
No user is created in my UserAuthRepository (OrmLiteAuthRepository). The User Id stored in the session is invalid. Any method decorated with the [Authenticate] attribute causes a 404 (User not found) error to be returned.
I would expect that a user is created with the First/Last/Email obtained from Facebook or Twitter. Am I misunderstanding something?
ServiceStack v4.0.38
A few things to check would be the oauth.CallbackUrl has been set correctly taking into account the multiple providers you have registered. Eg http://localhost/auth/{0}.
Also you should check if your IDbConnectionFactory used with your OrmLiteAuthRepository has also been registered with the IoC container. Eg
var dbFactory = new OrmLiteConnectionFactory(
"~/App_Data/db.sqlite".MapHostAbsolutePath(),
SqliteDialect.Provider);
container.Register<IDbConnectionFactory>(dbFactory);
var authRepo = new OrmLiteAuthRepository(dbFactory);
container.Register<IUserAuthRepository>(authRepo);
authRepo.InitSchema();
I wanted to update this thread to say that I've upgraded to ServiceStack 4.0.42 and this issue has resolved itself.
Related
I have this controller
[Route("Authentication")]
public class AuthenticationController : Controller
{
and this action
[HttpGet("SignOut")]
public async Task<IActionResult> SignOut([FromQuery] string sid)
{
await ControllerContext.HttpContext.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme);
await ControllerContext.HttpContext.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme);
return View();
This works as expected.
But when I configure a SignedOutCallbackPath for my OpenId authentication that has the same route, it doesn't work anymore. The constructor of my controller is not called, the action is not hit and the result in the browser is a blank page (code 200) with html/head/body, but all empty, that doesn't match any template or view.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.HttpOnly = true;
})
.AddOpenIdConnect(options =>
{
options.SignedOutCallbackPath = "/Authentication/SignOut";
Is the SignedOutCallbackPath not supposed to be a view of my own?
The callback paths in the OpenID Connect authentication scheme are internal paths that are used for the authentication flow of the OpenID Connect protocol. There are three of those:
CallbackPath – The path the authentication provider posts back when authenticating.
SignedOutCallbackPath – The path the authentication provider posts back after signing out.
RemoteSignOutPath – The path the authentication provider posts back after signing out remotely by a third-party application.
As you can see from my explanation, these are all URLs that the authentication provider uses: They are part of the authentication flow and not to be directly used by your users. You also don’t need to worry about handling those things. The OpenID Connect authentication handler will automatically respond to these requests when the authentication middleware runs.
This means that when you change a callback path to some path that is a route of one of your controller actions, then the authentication middleware will handle that request before your controller gets involved. This is by design, so that you do not need to worry about these routes as they are mostly internal.
You just have the option to change those paths if you cannot or do not want to use the default values.
Now, there are two possible things I can think of that you could have meant to change instead:
SignedOutRedirectUri: This is the URL the user gets redirected to after the sign-out process is completed. This basically allows you to send the user to some view with e.g. a message “you were successfully signed out”, to show that the sign-out is done.
You can also set this as part of the AuthenticationProperties that you can pass to the SignOutAsync.
CookieAuthenticationOptions.LogoutPath: This is the URL that is configured to the actual URL that users can go to to sign out of the application. This does not really have that much of an effect though.
Otherwise, it’s really up to you to send users to your /Authentication/SignOut URL. You can put a button into your layout that goes there for example, to offer users a sign out functionality at all times.
Your Action is expecting parameter which is not passed by your callback, the parameter is seemingly not used within the action either, so you can either omit the parameter or make it optional
[HttpGet("SignOut")]
public async Task<IActionResult> SignOut()
{
await ControllerContext.HttpContext.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme);
await ControllerContext.HttpContext.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme);
return View();
}
For Azure AD I found this post logout redirection behavior:
It doens't work for root accounts of the tenant, that is my personal account, which created Azure
subscription.
But it works for new accounts I created inside of my
subscription.
I have created a MVC/API project to enable external authentication and worked fine for my local host url. However, I need to achieve the below.
I am supporting multi tenancy (same app service and different DB), so each tenant has to connect different DB based on the custom param in the MVC url
Ex: https://localhost/tenant1, .../tenant2, .../tenant3 etc (not going with separate subdomain at this point)
I am not sure if the Google Console supports the wildcard url as a return ur and not sure how to achieve that in MVC code (Ex:http://localhost/* OR {0} .. something like that. (So dynamic input parameter will be returned back from google)
I am reading and attempting some solutions. Will update the answer here once i get the complete solution. In the meantime if anyone has any suggestions, please help me.
UPDATE 1:
I have updated my source code as follows:
Create session object before redirecting to the external login
System.Web.HttpContext.Current.Session["Tenant"] = "tenantname";
After callback read the tenant details and save in the session for subsequent DB calls based on the tenant name
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
if (System.Web.HttpContext.Current.Session["Tenant"] != null)
{
string sessionObj = System.Web.HttpContext.Current.Session["Tenant"] as String;
}
This is a common requirement, and is easily solved. There are two components.
Firstly, regardless of which of your many URLs your application lives at (myapp.com/tenant1, /tenant2, etc) you have a single redirect URL (eg myapp.com/oauthredirect).
Secondly, when starting the OAuth dance (https://developers.google.com/identity/protocols/OAuth2WebServer#redirecting), you can specify a state parameter which will be passed into your oauthredirect routine (eg. as state=tenant1). You can then use this to create a redirect back to the appropriate site URL once you have finished your user registration tasks.
Be careful when specifying your redirect URLs into the developer console. They must be a character-by-character match with the actual URL. So, foe example, you will need to specify both http://myapp.com/oauthredirect and https://myapp.com/oauthredirect. I've always found it quite useful to create a local entry in /etc/hosts (or the windows equivalent) so your localhost is also resolved by eg. http://test.myapp.com
Authorized redirect URIs For use with requests from a web server. This
is the path in your application that users are redirected to after
they have authenticated with Google. The path will be appended with
the authorization code for access. Must have a protocol. Cannot
contain URL fragments or relative paths. Cannot be a public IP
address.
http://localhost/google-api-php-client-samples/Analytics/Oauth2.php
http://localhost/authorize/
You can have as many of them as you want but the wild card is not going to work.
I am experiencing a strange issue with my mvc 5 web application that I can't seem to figure out. The Application is an internal web app I've built for my organization. Primarily it is accessed by employees who use domain connected computers but also will access via mobile devices and tablets. I wanted to provide the former an automatic login experience through windows auth and AD (without having to enter credentials if they were already signed on to the domain) I also wanted to be able to provide all other users with a custom login screen rather than the browser native prompt. To implement this I created a separate web app which authenticates the windows users and sends an encrypted cookie back to the main app with the user's roles. Non windows based browsers are presented with a login page in the main app that authenticates against AD and retrieves the user's roles. For each type of login the roles are than converted to claims and a federated token is created for the user.
My problem is that when a user logs in via the redirect to the windows auth app a strange issue is occuring. Any form that I submit whether it be standard form submit or an AJAX post has to be submitted within a minute of loading the page otherwise the parameters sent to the controller action do not bind (null). If a user logins in via the custom login page this problem doesn't exist.
Here is the code that performs the initial authentication in global.asax:
Protected Sub Application_AuthenticateRequest()
Dim user As System.Security.Principal.IPrincipal = HttpContext.Current.User
If user Is Nothing Then
'First check if an authentication cookie is has been generated from the windows login
'authentication app
Dim authCookie As HttpCookie = Request.Cookies(".ConnectAUTH")
If Not authCookie Is Nothing Then
' Extract the roles from the cookie, and assign to our current principal, which is attached to the HttpContext.
Dim ticket As FormsAuthenticationTicket = FormsAuthentication.Decrypt(authCookie.Value)
Dim claims As New List(Of Claim)
For Each role In ticket.UserData.Split(";"c)
claims.Add(New Claim(ClaimTypes.Role, role))
Next
Dim claimIdent As New ClaimsIdentity(claims, "Custom")
claimIdent.AddClaim(New Claim(ClaimTypes.WindowsAccountName, ticket.Name))
claimIdent.AddClaim(New Claim(ClaimTypes.NameIdentifier, ticket.Name))
Dim claimPrinc As New ClaimsPrincipal(claimIdent)
Dim token = New SessionSecurityToken(claimPrinc)
Dim sam = FederatedAuthentication.SessionAuthenticationModule
sam.WriteSessionTokenToCookie(token)
HttpContext.Current.User = New ClaimsPrincipal(claimIdent)
Return
Else 'User hasn't been authenticated
Dim ConnectBaseURL = Request.Url.GetLeftPart(UriPartial.Authority) & "/"
Dim mvcPath As String = Request.Url.ToString.Replace(ConnectBaseURL, "")
'If user is requesting the login page then let them authenticate through there
If mvcPath.ToUpper.Contains("ACCOUNT/LOGIN") Or mvcPath.ToUpper.Contains("ACCOUNT/LOGUSERIN") Or mvcPath.ToUpper.Contains("SIGNALR") Then Exit Sub
'for brevity i will omit the code below:
' Basically it checks whether the browser is windows based if so then it redirects the user to
' a windows login authenticator which authenticates the user against Active Directory, builds and sends
' an encrypted cookie with a list of roles/groups that the user belongs to. When this function detects that cookie
' it decrypts it, sets up a claims identity, add the user roles and creates a federated authentication token
'If the the browser is not windows based then it redirects the user to a custom login page which is tied to an MVC
'action that will also authenticate the user against active directory and and set up the claims identity ...
End If
End If
End Sub
Here is the code that authenticates the user if they are redirected to the custom login page:
<AllowAnonymous>
<HttpPost>
<ValidateAntiForgeryToken>
<OutputCache(NoStore:=True, Duration:=0, Location:=OutputCacheLocation.None, VaryByParam:="None")>
Public Function LogUserIn(model As User, returnURL As String) As ActionResult
Try
If ModelState.IsValid Then
If Membership.ValidateUser(model.UserName, model.PassWord) Then
'Call helper function to get all user roles and convert them to claims
Dim userClaims As List(Of Claim) = New LDAPHelper().GetUserGroups(model.UserName)
userClaims.Add(New Claim(ClaimTypes.WindowsAccountName, model.UserName))
userClaims.Add(New Claim(ClaimTypes.NameIdentifier, model.UserName))
userClaims.Add(New Claim(ClaimTypes.Name, model.UserName))
Dim claimIdent As New ClaimsIdentity(userClaims, "Custom")
Dim claimPrinc As New ClaimsPrincipal(claimIdent)
Dim token = New SessionSecurityToken(claimPrinc)
Dim sam = FederatedAuthentication.SessionAuthenticationModule
sam.WriteSessionTokenToCookie(token)
If returnURL Is Nothing Then
Return Redirect("~/")
Else
Return Redirect(returnURL)
End If
Else
ModelState.AddModelError("LoginFailure", "The username/password combination was invalid")
End If
End If
Return Nothing
Catch ex As Exception
ModelState.AddModelError("LoginFailure", ex.Message)
Return Nothing
End Try
End Function
I've tried eliminating the forms cookie from the equation by not sending it from the windows auth app and just hardcoding the claims and token creation after being redirected back to the main app. The HttpContext.Current.User object stays set as a valid claimsPrincipal each time Application_AuthenticateRequest is hit. I've implemented a Custom AuthorizeAttribute and the user is always authenticated and authorized. The funny thing is if i hit the submit button on the form again immediately after the parameters passed through as null, it works. I've scoured online for a similar problem - nothing - i'm hoping someone on here has an idea.
My answer might be completely unrelated but I'm justtrying to help. We had something similar. When we switched from a web api in MVC4 to MVC5. We had an AuthorizationAttribute that checked the token. We read it, set the principal but when we hit the controller action, the principal was gone (in mVC5 - in the previous version this worked). The short story was that it had to do with the async nature of MVC5. We changed our way to implement another attribute that implemented IAuthenticationFilter (which is where mVC5 expects the principal to be set).
What this has to do with you ? I think you also have a problem with async and that you need to figure out in which event MVC5 expects you to set the principal (Application_AuthenticateRequest might not be the place). Anything else might be set on the wrong thread and be gone when you arrive in the controller.
I making a facebook login using the new setup in ASPNET mvc 5. When the user login I can see that there is a request for the default public profile, but all I'm getting back is the token, username and nothing else.
I thought that by default I would also get information like first and lastname. And if added to the scope also the email.
On the callback from facebook I'm having this code to extract the information:
var authManager = HttpContext.GetOwinContext().Authentication;
var loginInfo = await authManager.GetExternalLoginInfoAsync();
Thanks to the comment, I was able to find out that I needed to look into the identity of the callback to get the requested information
var loginIdentity = await authManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
This will provide everything beside the logintoken/provider.
I needed to look at the ExternalIdentity and not the ExternalLoginInfo. The post is updated with this info. Also important to add email to the scope
I am using successfully custom authorization in ASP.NET MVC. It simply involves a comparison between User.Identity and the owner of the object in context.
It works like a charm when used in simple conditions. It becomes more complicated when I try to call 2 actions in one web request.
Lets say I want to display an image which would be generated on-the-fly by my application. This image is generated by a controller, thus, it can be referenced by an URL even if it doesn't exist physically. I have decided that the user must be signed in and be the owner to view it, so I apply my authorization mechanizm to it.
Example: <img src="http://myapplication.com/images/generate/3" />
When I include such an image in a page via its action hyperlink, I expect that the authenticated user will still be in context on server side when the image is generating. This is not the case in my tests. The image never displays because my authorization check doesn't work. In the image controller, User.Identity is empty as if the user has not signed it.
In the meantime, the same user is still signed in to the website and can continue to browse with his identity in context... without those images working properly.
I wonder how to make this process work securely...
Thank you very much!
Marc Lacoursiere
RooSoft Computing inc.
Just wondering if you've checked if
Thread.CurrentPrincipal
is also empty in the controller? It should contain the same value.
Another suggestion would be to store the User.Identity value in a session?
You need to set up your identity in global.asax on every request. I'm using a custom Principal and Identity for this.
private void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (!Request.IsAuthenticated)
{
SetIdentity(new MyIdentity
{ Type = UserType.Inactive, Id = int.MinValue });
}
else
{
HttpCookie authCookie = Request.Cookies[
FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
FormsAuthenticationTicket authTicket =
FormsAuthentication.Decrypt(authCookie.Value);
var identity = Repository.GetIdentity
(authTicket.Name, new HttpRequestWrapper(Request));
SetIdentity(identity);
}
}
}
private void SetIdentity(MyIdentity identity)
{
Context.User = new MyPrincipal { Identity = identity };
Thread.CurrentPrincipal = Context.User;
}
This works, but I don't guarantee it to be secure. You should review this article on FormsAuthentication vulnerabilities prior to going live with this code. You have to understand that this code assumes the cookie is valid and hasn't been hijacked. There are some additional security measures that can be taken to reduce these vulnerabilities which this code doesn't show.
This may be when the site link in browser is http:\www.mysite.com (or http:\subdomain.mysite.com ) and you are using http:\mysite.com\image\5 in your application. Form authentication uses cookies. And these cookies may belong to domains and subdomains.
To find out what is going on I suggest to use FireFox with FireBug installed. Enable Net and Console tab for your site and make a complete refresh of the page. After you'll see requests in one of these tabs (Net tab exactly). At the left of the request you can see 'a plus' button, after you click it you'll see Headers and Response tabs (more detailed description of firebug). Have a look at Headers tab and try to find something like FORMAUTH (or what you've set in config as a forms cookie name). If you not see it - the problem is in domains.