IdentityServer3 Refresh Token - Where to invoke the refresh - owin

I am developing a MVC application that uses OpenID and IdentityServer3.
Background:
I am running into a issue that when the Authentication Cookie times out, I need to use the refresh token to generate a new one.
I am able to login and receive the AuthorizationCodeReceived notification, which i use to request an authorization code and retrieve a RefreshToken which I store in the claims of the AuthenticationTicket.
I have tried adding logic to check and refresh the authentication in:
CookieAuthenticationProvider.OnValidateIdentity -- This works to
refresh, and I was able to update the cookie, but it is not called after the cookie expired.
Adding code in the begining of the the ResourceAuthorizationManager.CheckAccessAsync -- this does not work because the identity is null and I cannot retrieve the refresh token claim.
Adding a filter Filter for MVC, but I am unable to figure out what to add as a HttpResponseMessage for WebAPI.
public const string RefreshTokenKey = "refresh_token";
public const string ExpiresAtKey = "expires_at";
private const string AccessTokenKey = "access_token";
private static bool CheckAndRefreshTokenIfRequired(ClaimsIdentity id, out ClaimsIdentity identity)
{
if (id == null)
{
identity = null;
return false;
}
if (id.Claims.All(x => x.Type != ExpiresAtKey) || id.Claims.All(x => x.Type != RefreshTokenKey))
{
identity = id;
return false;
}
//Check if the access token has expired
var expiresAt = DateTime.Parse(id.FindFirstValue(ExpiresAtKey));
if ((expiresAt - DateTime.Now.ToLocalTime()).TotalSeconds < 0)
{
var client = GetClient();
var refreshToken = id.FindFirstValue(RefreshTokenKey);
var tokenResponse = client.RequestRefreshTokenAsync(refreshToken).Result;
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
var result = from c in id.Claims
where c.Type != AccessTokenKey &&
c.Type != RefreshTokenKey &&
c.Type != ExpiresAtKey
select c;
var claims = result.ToList();
claims.Add(new Claim(AccessTokenKey, tokenResponse.AccessToken));
claims.Add(new Claim(ExpiresAtKey, DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
claims.Add(new Claim(RefreshTokenKey, tokenResponse.RefreshToken));
identity = new ClaimsIdentity(claims, id.AuthenticationType);
return true;
}
identity = id;
return false;
}
Links:
How would I use RefreshTokenHandler?
Identity Server3 documentation
Looked at the two examples, but using resourceowner flow for openid doesn't seem the right way. The MVC code flow relies on the User still having the principle, but my claims are all empty in the resource authorize.
EDIT:
Okay, so if I set the AuthenticationTicket.Properties.ExpiresUtc to null in AuthorizationCodeReceived, it is setting it to null then somewhere down the line it is setting it to 30 days instead of 5 minutes (I searched the katana and identity server source code but could not find where it is setting this value), which I can live with, but would prefer it to be the same as the browser where it is "Session"
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieManager = new SystemWebChunkingCookieManager(),
Provider = new CookieAuthenticationProvider()
{
OnValidateIdentity = context =>
{
ClaimsIdentity i;
if (CheckAndRefreshTokenIfRequired(context.Identity, out i))
{
context.ReplaceIdentity(i);
}
return Task.FromResult(0);
}
}
});

The problem was that in the AuthorizationCodeRecieved notification I was passing the Properties from the original ticket, which had the timeout set for Expires for the authorization code Changing the the code to pass null in resolved the issue and allowed the CookieAuthenticationHandler.ApplyResponseGrantAsync to pass its own properties.
var claimsIdentity = new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role");
n.AuthenticationTicket = new AuthenticationTicket(claimsIdentity, null);

Related

Authentication problem - secure web application with API auth server

I have started to create a software architecture where i have:
Auth_API - as an auth server
Resource_API - as a resource API (protected with Auth_API)
WebApplication (mvc) - a frontend application (protected with Auth_API).
Based on https://bitoftech.net/2014/06/01/token-based-authentication-asp-net-web-api-2-owin-asp-net-identity/ article I have successfully made a Google authentication.
WebApp == redirects to ==> Auth_API == challenge ==> Google ==> API receives externalAccessToken, registers user locally and returns localAccessToken
Now everything is OK when I would want to use bearer authorization (using local access token).
But I also want to sign in to by ASP MVC application with (cookie?) ClaimsIdentity.
I was thinking about switching to JWT, but I am not sure which way I should go...
Bit of code:
Auth_API - obtain local access token
/// <summary>
/// Returns local access token for already registered users
/// </summary>
/// <param name="provider"></param>
/// <param name="externalAccessToken"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpGet]
[Route("ObtainLocalAccessToken")]
public async Task<IHttpActionResult> ObtainLocalAccessToken(string provider, string externalAccessToken)
{
if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(externalAccessToken))
{
return BadRequest("Provider or external access token is not sent");
}
var verifiedAccessToken = await VerifyExternalAccessToken(provider, externalAccessToken);
if (verifiedAccessToken == null)
{
return BadRequest("Invalid Provider or External Access Token");
}
IdentityUser user = await _repo.FindAsync(new UserLoginInfo(provider, verifiedAccessToken.user_id));
bool hasRegistered = user != null;
if (!hasRegistered)
{
return BadRequest("External user is not registered");
}
//generate access token response
var accessTokenResponse = GenerateLocalAccessTokenResponse(user.UserName);
return Ok(accessTokenResponse);
}
Generate Local Access Token algorythm
private JObject GenerateLocalAccessTokenResponse(string userName)
{
var tokenExpiration = TimeSpan.FromDays(1);
ClaimsIdentity identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, userName));
identity.AddClaim(new Claim("role", "user"));
var props = new AuthenticationProperties()
{
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.Add(tokenExpiration),
};
var ticket = new AuthenticationTicket(identity, props);
var accessToken = Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket);
JObject tokenResponse = new JObject(
new JProperty("userName", userName),
new JProperty("access_token", accessToken),
new JProperty("token_type", "bearer"),
new JProperty("expires_in", tokenExpiration.TotalSeconds.ToString()),
new JProperty(".issued", ticket.Properties.IssuedUtc.ToString()),
new JProperty(".expires", ticket.Properties.ExpiresUtc.ToString())
);
return tokenResponse;
}
Web application - part of login action:
if (hasLocalAccount)
{
var client = new RestClient(baseApiUrl);
var externalLoginUrl = "Account/ObtainLocalAccessToken";
var externalLoginRequest = new RestRequest(externalLoginUrl, Method.GET);
externalLoginRequest.AddQueryParameter("provider", provider);
externalLoginRequest.AddQueryParameter("externalAccessToken", externalAccessToken);
var externalLoginResponse = client.Execute(externalLoginRequest);
if (externalLoginResponse.IsSuccessful)
{
JObject response = JObject.Parse(externalLoginResponse.Content);
string localAccessToken = response["access_token"].Value<string>();
string localTokenExpiresIn = response["expires_in"].Value<string>();
// WHAT TO DO WHERE TO SIGN IN A USER ???
//AuthenticationTicket ticket = Startup.OAuthBearerOptions.AccessTokenFormat.Unprotect(localAccessToken); <== this returns NULL
return RedirectToAction("Index", "Home");
}
}

Re-authorize persistent login (MVC client) without triggering login UI

I am trying to figure out how a server-side client (MVC / ASP.NET Core 2) can query IdentityServer4 to retrieve various claims scopes for a persistent login created in some previous session without prompting for login if the persistent login is invalid (user inactive, cookie expired, etc).
We're using Implicit flow with third-party auth (Google, FB, etc) but we changed the session-duration on the cookie to a more user-friendly 30 day expiration in IdentityServer's ExternalLoginCallback.
Accessing claims on HttpContext.User (we are not using ASP.NET Identity) works great during the session that establishes login. On some later session, navigating to a client resource with an [Authorize] attribute also works: if the user had logged in previously, they transparently gain access to the resource, claims are populated, etc. If not, they're prompted for login, which is ok in response to a user-initiated action.
However, we have a requirement for the client landing page to alter the content depending on whether the user is anonymous or authenticated. A simple example would be "Register" and "Log in" links for anonymous users, but "Account" and "Log out" links for authenticated users.
Hence the reason to retrieve claims and jump-start the persistent login if it's valid, but do nothing (no login prompt) if invalid: we don't want the landing page to force every anonymous user to a login screen.
Nothing special to say about our setups on either end of the pipeline. Client:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "https://localhost:5000";
options.RequireHttpsMetadata = true;
options.ClientId = "example.com.webserver";
options.ClientSecret = "examplesecret";
options.ResponseType = "id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("example.com.identity");
});
IdentityServer client resource definition:
new Client
{
ClientId = "example.com.webserver",
ClientName = "example.com",
ClientUri = "https://localhost:5002",
AllowedGrantTypes = GrantTypes.Implicit,
ClientSecrets = {new Secret("examplesecret".Sha256())},
RequireConsent = false,
AllowRememberConsent = true,
AllowOfflineAccess = true,
RedirectUris = { "https://localhost:5002/signin-oidc"},
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc"},
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Phone,
IdentityServerConstants.StandardScopes.Address,
"example.com.identity"
}
}
The client runs an intentional login (user clicks "Log in" link) like so:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task Login()
{
await HttpContext.SignOutAsync("oidc");
await HttpContext.ChallengeAsync("oidc",
new AuthenticationProperties() {
RedirectUri = Url.Action("LoginCallback")
});
}
The solution is to add a second OIDC auth flow in Setup, intercept the redirect to change the Prompt option to none so that no login prompt is shown, intercept the resulting login_required error message, and trigger that flow in the landing page's PageModel OnGet handler (the client app uses RazorPages).
One caveat is the handler must set flags so that this is only attempted once, and so that it can detect whether the page is being hit for the first time, or as the return-trip from the login attempt. This is achieved by just dropping a value into the Razor TempData which is just a cookie-based bucket of name-value pairs.
Add to Setup.cs
.AddOpenIdConnect("persistent", options =>
{
options.CallbackPath = "/signin-persistent";
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.Prompt = "none";
return Task.FromResult<object>(null);
},
OnMessageReceived = context => {
if(string.Equals(context.ProtocolMessage.Error, "login_required", StringComparison.Ordinal))
{
context.HandleResponse();
context.Response.Redirect("/");
}
return Task.FromResult<object>(null);
}
};
options.SignInScheme = "Cookies";
options.Authority = "https://localhost:5000";
options.RequireHttpsMetadata = true;
options.ClientId = "example.com.webserver";
options.ClientSecret = "examplesecret";
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("example.com.identity");
})
Landing page (Index.cshtml.cs)
public class IndexModel : PageModel
{
private bool PersistentLoginAttempted = false;
private const string PersistentLoginFlag = "persistent_login_attempt";
public IActionResult OnGet()
{
// Always clean up an existing flag.
bool FlagFound = false;
if(!String.IsNullOrEmpty(TempData[PersistentLoginFlag] as string))
{
FlagFound = true;
TempData.Remove(PersistentLoginFlag);
}
// Try to refresh a persistent login the first time an anonymous user hits the index page in this session
if(!User.Identity.IsAuthenticated && !PersistentLoginAttempted)
{
PersistentLoginAttempted = true;
// If there was a flag, this is the return-trip from a failed persistent login attempt.
if(!FlagFound)
{
// No flag was found. Create it, then begin the OIDC challenge flow.
TempData[PersistentLoginFlag] = PersistentLoginFlag;
return Challenge("persistent");
}
}
return Page();
}
}
why not just make the authentication cookie permanent? Here's an other way to do it ... You can then check authentication on the server against the authentication server.
on the client AddOpenIdConnect :
options.Events = new OpenIdConnectEvents {
OnRedirectToIdentityProvider = context => {
context.Properties.RedirectUri = context.Request.Path;
return Task.FromResult(0);
},
OnTicketReceived = context =>
{
context.Properties.IsPersistent = true;
context.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddDays(15);
context.Properties.AllowRefresh = true;
return Task.FromResult(0);
}
};

What does GenerateCorrelationId() and ValidateCorrelationId() do?

I see this code within a custom owin handler to do Oauth2. For example here: https://github.com/RockstarLabs/OwinOAuthProviders/blob/master/Owin.Security.Providers/Reddit/RedditAuthenticationHandler.cs
Can someone explain to me in plain English what these two methods do in the context of oauth2? It seems to be related to CSRF but not sure how.
When a redirect to an "OAuth 2" partner occurs there must be someway of correlating the eventual redirect back to your own application with the original redirect that you sent.
The way the Microsoft.Owin AuthenticationHandler accomplishes this:
generates a nonce of random bytes and retains it in a browser cookie
(GenerateCorrelationId)
encrypts this nonce and other information and your job is to pass this in a state query string parameter to the partner (recall that the partner's job is to return this value right back to your application after authenticating the user)
validates the nonce by decrypting the state query string parameter and verifying it matches the value in the cookie stored (ValidateCorrelationId)
Here is the source:
protected void GenerateCorrelationId(AuthenticationProperties properties)
{
if (properties == null)
{
throw new ArgumentNullException("properties");
}
string correlationKey = Constants.CorrelationPrefix +
BaseOptions.AuthenticationType;
var nonceBytes = new byte[32];
Random.GetBytes(nonceBytes);
string correlationId = TextEncodings.Base64Url.Encode(nonceBytes);
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = Request.IsSecure
};
properties.Dictionary[correlationKey] = correlationId;
Response.Cookies.Append(correlationKey, correlationId, cookieOptions);
}
protected bool ValidateCorrelationId(AuthenticationProperties properties,
ILogger logger)
{
if (properties == null)
{
throw new ArgumentNullException("properties");
}
string correlationKey = Constants.CorrelationPrefix +
BaseOptions.AuthenticationType;
string correlationCookie = Request.Cookies[correlationKey];
if (string.IsNullOrWhiteSpace(correlationCookie))
{
logger.WriteWarning("{0} cookie not found.", correlationKey);
return false;
}
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = Request.IsSecure
};
Response.Cookies.Delete(correlationKey, cookieOptions);
string correlationExtra;
if (!properties.Dictionary.TryGetValue(
correlationKey,
out correlationExtra))
{
logger.WriteWarning("{0} state property not found.", correlationKey);
return false;
}
properties.Dictionary.Remove(correlationKey);
if (!string.Equals(correlationCookie, correlationExtra, StringComparison.Ordinal))
{
logger.WriteWarning("{0} correlation cookie and state property mismatch.",
correlationKey);
return false;
}
return true;
}

Manual force-authentication of a user without issuing an authentication request

I have a ServiceStack application that coexists with mvc5 in a single web project. The only purpose of the mvc5 part is to host a single controller action that receives a callback from janrain for javascript initiated social login. I could receive this callback in a SS service request, too, but then I don't know how I would do a redirect to the returnUrl that is passed through all the way from the javascript context. Even if I was able to figure this out, my question would still be the same.
Inside of the controller action, once I verify the janrain provided token resolves to a user in my system, I need to manually tell ServiceStack "hey trust me - this person is authorized".
All my searches lead to some code along the lines of the following snippet:
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
});
My first problem here is that I store hashed passwords (I support social login as well as manual login), so I don't know the user's password (and I shouldn't).
My second problem is that this code seems to only work for SS 3.X and not 4.X. I requires a ServiceStack.ServiceInterface.dll that is mysteriously missing from 4.X.
Is there a short and precise way to manually authenticate a user with SS on the server side?
Thanks
EDIT:
So far this is what I am doing: (This is not final code - I have commented out some things I don't know what to do with):
public class UsernameOnlyAuthorizationService : Service
{
public object Post(UsernameOnlyLoginRequest request)
{
var authProvider = new UsernameOnlyAuthProvider();
authProvider.Authenticate(this, GetSession(), new Authenticate()
{
UserName = request.username,
Password = "NotRelevant",
RememberMe = true
});
return HttpResult.Redirect(request.returnUrl);
}
}
public class UsernameOnlyAuthProvider : CredentialsAuthProvider
{
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
var authRepo = authService.TryResolve<IAuthRepository>().AsUserAuthRepository(authService.GetResolver());
ReferScienceDataContext db = authService.TryResolve<ReferScienceDataContext>();
var session = authService.GetSession();
IUserAuth userAuth;
var user = db.Users.FirstOrDefault(u => u.Username == userName);
if (user != null)
{
//AssertNotLocked(userAuth);
//session.PopulateWith(userAuth);
session.Id = user.Id.ToString();
session.UserName = user.Username;
session.FirstName = user.FirstName;
session.LastName = user.LastName;
session.IsAuthenticated = true;
session.UserAuthId = user.Id.ToString(CultureInfo.InvariantCulture);
session.ProviderOAuthAccess = authRepo.GetUserAuthDetails(session.UserAuthId)
.ConvertAll(x => (IAuthTokens)x);
return true;
}
return false;
}
}
And from within my Janrain success callback code I call it so:
HostContext.ResolveService<UsernameOnlyAuthorizationService>().Post(new UsernameOnlyLoginRequest() {username = user.Username, returnUrl= returnUrl});
This seems to work nicely, however, I can't get it to remember my session across browser closes. I am hardcoding RememberMe = true - why is this not working?
I would do this by creating an internal service, which you can call from your MVC5 controller action, where you only require to pass the username of the user you have authenticated.
public class JanrainSuccessService : Service
{
public void CreateSessionFor(string username)
{
var repository = TryResolve<IAuthRepository>().AsUserAuthRepository(GetResolver());
var user = repository.GetUserAuthByUserName(username);
var session = GetSession();
session.PopulateWith(user);
session.IsAuthenticated = true;
session.UserAuthId = user.Id.ToString(CultureInfo.InvariantCulture);
session.ProviderOAuthAccess = repository.GetUserAuthDetails(session.UserAuthId).ConvertAll(x => (IAuthTokens)x);
}
}
The code in this method, is effectively the same could that is used by the CredentialsAuthProvider, but has the advantage of not requiring the password of the user. (See the TryAuthenticate method here for original code)
In your MVC5 controller action method you would need to call:
HostContext.ResolveService<JanrainSuccessService>().CreateSessionFor(user.user_id);
This assumes that you have a valid repository of users configured to match username's against.
You should update your code to be:
public class UsernameOnlyAuthorizationService : Service
{
public object Post(UsernameOnlyLoginRequest request)
{
var authProvider = new UsernameOnlyAuthProvider();
authProvider.Authenticate(this, GetSession(), new Authenticate()
{
UserName = request.username,
Password = "NotRelevant",
RememberMe = true
});
// Remember the session
base.Request.AddSessionOptions(SessionOptions.Permanent);
return HttpResult.Redirect(request.returnUrl);
}
}
public class UsernameOnlyAuthProvider : CredentialsAuthProvider
{
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
var authRepo = authService.TryResolve<IAuthRepository>().AsUserAuthRepository(authService.GetResolver());
ReferScienceDataContext db = authService.TryResolve<ReferScienceDataContext>();
var session = authService.GetSession();
var user = db.Users.FirstOrDefault(u => u.Username == userName);
if (user == null)
return false;
session.Id = user.Id.ToString();
session.UserName = user.Username;
session.FirstName = user.FirstName;
session.LastName = user.LastName;
session.IsAuthenticated = true;
session.UserAuthId = user.Id.ToString(CultureInfo.InvariantCulture);
session.ProviderOAuthAccess = authRepo.GetUserAuthDetails(session.UserAuthId).ConvertAll(x => (IAuthTokens)x);
return true;
}
}

Obtaining Access token from refresh token using Google API

I have been using google+ api for .NET in my application.Using the example provided in this
site i am able to get the access token.
My problem is how to obtain the access token from the refresh token using OAuth 2.0.I haven't found any examples to get the access token from refresh token.
I have referred the [google+ API reference] but they have mentioned it using HTTP methods.2Please provide me some examples in C# using the methods provided by google+ APIs.
For the first time, you need to get the access token from the browser prompt and then save it in some store.
Check if the token is expired and then try to refresh it.
Code here :
private static IAuthorizationState GetAuthentication(NativeApplicationClient arg)
{
try
{
// Get the auth URL:
var config = new Configuration();
var calendarScope = Google.Apis.Util.Utilities.ConvertToString(CalendarService.Scopes.Calendar);
IAuthorizationState state = new AuthorizationState(new[] { calendarScope });
state.Callback = new Uri(NativeApplicationClient.OutOfBandCallbackUrl);
Uri authUri = arg.RequestUserAuthorization(state);
var authCode = String.Empty;
if (String.IsNullOrWhiteSpace(config.AccessToken) || config.AccessTokenExpirationTime < DateTime.Now || String.IsNullOrWhiteSpace(config.RefreshToken))
{
// Request authorization from the user (by opening a browser window):
Process.Start(authUri.ToString());
do
{
authCode = Prompt.ShowDialog("Test", "123");
} while (String.IsNullOrWhiteSpace(authCode));
state = arg.ProcessUserAuthorization(authCode, state);
}
else
{
state.AccessToken = config.AccessToken;
state.AccessTokenExpirationUtc = config.AccessTokenExpirationTime;
state.AccessTokenIssueDateUtc = config.AccessTokenIssueTime;
state.RefreshToken =config.RefreshToken ;
if (state.AccessTokenExpirationUtc < DateTime.Now)
{
var tokenRefreshed = arg.RefreshToken(state);
if (tokenRefreshed)
{
config.AccessToken = state.AccessToken;
config.AccessTokenExpirationTime = state.AccessTokenExpirationUtc;
config.AccessTokenIssueTime = state.AccessTokenIssueDateUtc;
config.RefreshToken = state.RefreshToken;
arg.ProcessUserAuthorization(authCode, state);
}
else
{
throw new ApplicationException("Unable to refresh the token.");
}
}
}
return state;
}
catch (System.Exception exp)
{
throw new ApplicationException("Failed to get authorisation from Google Calender.", exp);
}
}

Resources