Active Directory Access Token expire detection - c#

I am using this singleton pattern on accessToken. For preventing unnecessary token call to login.windows service.
As you see if I have access token, I am serving existing token to my Code.
My problem is when I retrieve token first time. There is no Expire Time or any information.
This Expire on information always same as request date :
private static string accessToken_;
private static string AccessToken
{
get
{
if (accessToken_ == null)
{
accessToken_ = GetAccessToken();
}
return accessToken_;
}
}
private static string GetAccessToken()
{
string AadInstance = "https://login.windows.net/{0}";
AuthenticationContext authenticationContext = new AuthenticationContext(string.Format(AadInstance, aadTenantId));
ClientCredential clientCredential = new ClientCredential(clientId, clientSecretKey);
AuthenticationResult authenticationResult = authenticationContext.AcquireTokenAsync(ResourceId, clientCredential).Result;
return authenticationResult.AccessToken;
}
And then really I cannot know when my token will expire?!
For fixing this problem I have this code block on Exception Catch.
And I think it is not correct design. Do you have another information with suitable design? :
private static CellSet ExecuteOlapCellSet(string mdxQuery)
{
try
{
.... method codes here...
}
catch (Exception e)
{
//25 November: unauthorized, probably token expired.
if (e.InnerException != null && e.InnerException.Message.Contains("(401)"))
{
accessToken_ = GetAccessToken();
//call same method Again here
ExecuteOlapCellSet(mdxQuery);
}
Utils.LogError(ErrorVariables.Error105, e.InnerException ?? e);
throw new AnalystServiceException(ErrorVariables.Error105, e);
}
}

Related

Google.Apis.Auth.OAuth2.Mvc library and refresh token

I'm trying to retrieve the reviews from our business account.
For this I'm using the Google.Apis.Auth.OAuth2.Mvc library for .net https://www.nuget.org/packages/Google.Apis.Auth.MVC/ and following this example https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web-applications-asp.net-mvc.
The library is supposed to use the refresh token automatically but for some reason after 1 hour when the access token expires we lost access to the reviews.
Here is my implementation:
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "xxxxxxxxxxxxxxxxxxxxxxxxx",
ClientSecret = "xxxxxxxxxxxxxxxxxxxxx"
},
Scopes = new string[] { "https://www.googleapis.com/auth/plus.business.manage" },
DataStore = new FileDataStore(System.Web.HttpContext.Current.Server.MapPath("/App_Data/MyGoogleStorage"), true)
//DataStore = new FileDataStore(System.Web.HttpContext.Current.Server.MapPath("/App_Data/Drive.Api.Auth.Store"))
});
public override string GetUserId(Controller controller)
{
return "our email address";
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
public async Task<ActionResult> IndexAsync(CancellationToken cancellationToken)
{
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
AuthorizeAsync(cancellationToken);
if (result.Credential != null)
{
var accessToken = result.Credential.Token.AccessToken;
var client = new RestClient("https://mybusiness.googleapis.com/v4/accounts/116326379071192580211/locations/6608127685860731136/reviews?access_token=" + accessToken);
var request = new RestRequest(Method.GET);
IRestResponse response = client.Execute(request);
GoogleReviewsModel googleReviews = Newtonsoft.Json.JsonConvert.DeserializeObject<GoogleReviewsModel>(response.Content);
return View("Index", googleReviews);
}
else
{
return new RedirectResult(result.RedirectUri);
}
}
public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
{
protected override Google.Apis.Auth.OAuth2.Mvc.FlowMetadata FlowData
{
get { return new AppFlowMetadata(); }
}
}
The reviews are from our own company so we don't to logins from different users. What I want to achieve is to login the first time with our company logins and then automatically refresh the access token with the refresh token so the reviews are always visible in the website,
Thanks a lot!
EDIT:
After 1 hour the response I get from the following code is this:
var accessToken = result.Credential.Token.AccessToken;
var client = new RestClient("https://mybusiness.googleapis.com/v4/accounts/116326379071192580211/locations/6608127685860731136/reviews?access_token=" + accessToken);
var request = new RestRequest(Method.GET);
IRestResponse response = client.Execute(request);
""message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.\",\n "status": "UNAUTHENTICATED"
result.Credential contains both the access token and the refresh token so it seems to read the file in app_data. But the access code seems to be expired and is not being refreshed at this point and is not asking to login again neither:
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
AuthorizeAsync(cancellationToken);
You are not actually "telling the library" to refresh the access token, you are directly using the token that had been stored, and is now expired. Your code that looks like this:
var accessToken = result.Credential.Token.AccessToken;
should look like this:
var accessToekn = await result.Credential.GetAccessTokenForRequestAsync();
The GetAccessTokenForRequestAsync method will check if the access token needs refreshing and do so when needed.

EWS Connection Giving Unauthorized (401) Error

I have been working on a program that scans an exchange inbox for specific emails from a specified address. Currently the program reads the inbox, downloads the attachment, and moves the email to another folder. However, after about 15 pulls from the EWS server, the connection starts giving a 401 Unauthorized error until I restart the program. The program is setup to login via OAuth as basic auth is disabled by the system administrator. Below is the code that I am using to obtain the exchange connection and read the emails from the inbox.
Exchange Connection Code:
public static async Task<ExchangeService> GetExchangeConnection()
{
var pcaOptions = new PublicClientApplicationOptions
{
ClientId = AppID,
TenantId = TenantID,
};
var pca = PublicClientApplicationBuilder.CreateWithApplicationOptions(pcaOptions).Build();
var ewsScopes = new string[] { "https://outlook.office365.com/EWS.AccessAsUser.All" };
var securePassword = new SecureString();
foreach (char c in Pasword)
securePassword.AppendChar(c);
try
{
var authResult = await pca.AcquireTokenByUsernamePassword(ewsScopes, Username, securePassword).ExecuteAsync();
ExchangeService exchangeService = new ExchangeService()
{
Credentials = new OAuthCredentials(authResult.AccessToken),
Url = new Uri("https://outlook.office365.com/ews/exchange.asmx"),
};
return exchangeService;
}
catch
{
return null;
}
}
Email Retriever
public static List<Email> RetreiveEmails()
{
ExchangeService exchangeConnection = GetExchangeConnection().Result;
try
{
List<Email> Emails = new List<Email>();
TimeSpan ts = new TimeSpan(0, -5, 0, 0);
DateTime date = DateTime.Now.Add(ts);
SearchFilter.IsGreaterThanOrEqualTo EmailTimeFilter = new SearchFilter.IsGreaterThanOrEqualTo(ItemSchema.DateTimeReceived, date);
if (exchangeConnection != null)
{
FindItemsResults<Item> findResults = exchangeConnection.FindItems(WellKnownFolderName.Inbox, EmailTimeFilter, new ItemView(10));
foreach (Item item in findResults)
{
if (item.Subject != null)
{
EmailMessage message = EmailMessage.Bind(exchangeConnection, item.Id);
message.Load(new PropertySet(BasePropertySet.FirstClassProperties, ItemSchema.TextBody));
Emails.Add(new Email(message.DateTimeReceived, message.From.Name.ToString(), message.Subject, message.TextBody.ToString(), (message.HasAttachments) ? "Yes" : "No", message.Id.ToString()));
}
}
}
exchangeConnection = null;
return Emails;
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
return null;
}
}
The error occurs when the email retriever tries to either create the exchange connection or when requesting the emails from the folder. In either case the code will error out and give me 401 unauthorized while using credentials that work for the first dozen times and then fails after so many attempts. I have tried it with multiple different accounts and the issue persists with all of them and I have made sure that the application is authorized to access the exchange inbox. Any suggestions or help is much appreciated.
After doing further tracing regarding the 401 error it resulted in an issue with the token reaching the end of it's 1 hour lifespan. This is due to the original OAuth token having an initial life of 1 hour. This however was able to be fixed by setting up code to automatically refresh the token when needed. Here is the code to address this issue for anyone else who comes across this problem.
Authentication Manager:
class AuthenticationManager
{
protected IPublicClientApplication App { get; set; }
public AuthenticationManager(IPublicClientApplication app)
{
App = app;
}
public async Task<AuthenticationResult> AcquireATokenFromCacheOrUsernamePasswordAsync(IEnumerable<String> scopes, string username, SecureString password)
{
AuthenticationResult result = null;
var accounts = await App.GetAccountsAsync();
if (accounts.Any())
{
try
{
result = await (App as PublicClientApplication).AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync();
}
catch (MsalUiRequiredException)
{ }
}
if (result == null)
{
result = await (App as PublicClientApplication).AcquireTokenByUsernamePassword(scopes, username, password).ExecuteAsync();
}
return result;
}
}
I am using direct username and password authentication but the line of code can be switched to getting the user authentication via interactive methods as well. The code essentially creates a new instance of the authentication manager with a PublicClientApplication used to initialize it which houses the appID and tenantID. After initializing, you can call the AquireATokenFromCacheOrUsernamePasswordAsync which will attempt to see if there is an account present to get a token against. Next it will attempt to retrieve the previously cached token or refresh the token if it expires in less than 5 minutes. If there is a token available it will return that to the main application. If there isn't a token available, it will acquire a new token using the username and password supplied. Implementation of this code looks something like this,
class ExchangeServices
{
AuthenticationManager Manager = null;
public ExchangeServices(String AppId, String TenantID)
{
var pcaOptions = new PublicClientApplicationOptions
{
ClientId = AppID,
TenantId = TenantID,
};
var pca = PublicClientApplicationBuilder.CreateWithApplicationOptions(pcaOptions).Build();
Manager = new AuthenticationManager(pca);
}
public static async Task<ExchangeService> GetExchangeService()
{
var ewsScopes = new string[] { "https://outlook.office365.com/EWS.AccessAsUser.All" }
var securePassword = new SecureString();
foreach(char c in Password)
securePassword.AppendChar(c);
var authResult = await Manager.AquireATokenFromCacheOrUsernamePasswordAsync(ewsScopes, Username, securePassword);
ExchangeService exchangeService = new ExchangeService()
{
Credentials = new OAuthCredentials(authResult.AccessToken),
Url = new Uri("https://outlook.office365.com/ews/exchange.asmx");
};
return exchangeService;
}
}
The code above is everything laid out that is needed to create a new authentication manager and use it to get and update new tokens while using EWS services through OAuth. This is the solution that I found to fix the issue described above.

Xamarin forms WebAuthenticator usage

I'm trying to implement Facebook Auth for Xamarin Forms App.
I'm doing everything like in this tutorial https://learn.microsoft.com/en-us/xamarin/essentials/web-authenticator?tabs=android so I'm using server side auth. Here is my mobile app code:
public class WebAuthViewModel:ObservableObject
{
private const string AuthenticationUrl = "https://myapp.com/mobileauth/";
private string _accessToken = "";
private bool _isAuthenticated = false;
public string AuthToken
{
get => _accessToken;
set => SetProperty(ref _accessToken, value);
}
public ICommand FacebookCommand { get; }
public WebAuthViewModel()
{
FacebookCommand = new Command(async()=>await OnAuthenticate("Facebook"));
}
async Task OnAuthenticate(string scheme)
{
try
{
WebAuthenticatorResult result = null;
var authUrl = new Uri(AuthenticationUrl + scheme);
var callbackUrl = new Uri("myapp://");
result = await WebAuthenticator.AuthenticateAsync(authUrl, callbackUrl);
AuthToken = string.Empty;
if (result.Properties.TryGetValue("name", out var name) && !string.IsNullOrEmpty(name))
{
AuthToken += $"Name: {name}{Environment.NewLine}";
}
if (result.Properties.TryGetValue("email", out var email) && !string.IsNullOrEmpty(email))
{
AuthToken += $"Email: {email}{Environment.NewLine}";
}
AuthToken += result?.AccessToken ?? result?.IdToken;
IsAuthenticated = true;
}
catch (Exception ex)
{
AuthToken = string.Empty;
}
}
}
Also I have some back-end code. All this works fine, I'm getting access token, UserId and so on.
But I still have some questions.
What is the right way to validate if login is still valid?
How should I authorize app actions?
And how could I implement Logout?
I will be grateful for advices or links.
As a user, you don’t want to have to sign in every time you use the app. Luckily, MSAL already caches your authorization and can log you in silently if it’s still valid.When properly authenticated we receive an access token that we can subsequently use to query other APIs that are secured by MSAL.
Signing out is pretty straight forward. We go through all the available accounts that MSAL has locally cached for us and sign them out. We also clear the access token that we stored in secure storage when we signed in.
public async Task<bool> SignOutAsync()
{
try
{
var accounts = await _pca.GetAccountsAsync();
// Go through all accounts and remove them.
while (accounts.Any())
{
await _pca.RemoveAsync(accounts.FirstOrDefault());
accounts = await _pca.GetAccountsAsync();
}
// Clear our access token from secure storage.
SecureStorage.Remove("AccessToken");
return true;
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
return false;
}
}

How to handle new refresh token on server side?

I have two projects. One is an Identity server 4 who handle users and authentication. The second need to use the first to login and ask for a token to access an API.
When I need to refresh the token I don't now how to handle the new access token. How I can set to the authentification asp dot net core the new token. All the refresh process are made on a AuthorizationHandler.
I tried to modify the claims on identity but doesnt work. I tried to stock access token and refresh token inside my own cookie but I have trouble because when I refresh the token I can only use them at the next request (I didn't achieve to modify the request.cookies only the response.cookies.
public static async Task SetToken(TokenKind token,string value, HttpContext context)
{
context.Response.Cookies.Append(GetTokenCookieName(token), value);
}
public static async Task<string> GetRefreshTokenAsync(HttpContext context)
{
return await SearchToken(TokenKind.Refresh,context);
}
private static async Task<string> SearchToken(TokenKind token, HttpContext context)
{
var tokenName = GetTokenName(token);
var test = context.Request.Cookies;
var apiToken = context.Request.Cookies.FirstOrDefault(x => x.Key == GetTokenCookieName(token)).Value;
if (apiToken == null)
{
// Save token into cookie
var tokenValue = await context.GetTokenAsync(GetTokenName(TokenKind.Access));
await SetToken(TokenKind.Access, tokenValue, context);
var refreshTokenValue = await context.GetTokenAsync(GetTokenName(TokenKind.Refresh));
await SetToken(TokenKind.Refresh, refreshTokenValue, context);
switch (token)
{
case TokenKind.Access:
return tokenValue;
case TokenKind.Refresh:
return refreshTokenValue;
default:
return null;
break;
}
}
else
{
return apiToken;
}
}
private async Task<bool> RefreshToken(AuthorizationFilterContext mvcContext, HttpClient client)
{
var refreshToken = await TokenUtils.GetRefreshTokenAsync(mvcContext.HttpContext);
//await mvcContext.HttpContext.GetTokenAsync("refresh_token");
var variables = new Dictionary<string, string>
{
{ "grant_type", "refresh_token" },
{ "client_id", _configuration["ApplicationOptions:ClientId"] },
{ "client_secret", _configuration["ApplicationOptions:ClientSecret"] },
{ "refresh_token", refreshToken }
};
var content = new FormUrlEncodedContent(variables);
var url = _configuration["ApplicationOptions:AuthorizeServer"] + "/connect/token";
var response = await client.PostAsync(url, content);
if (response.IsSuccessStatusCode == false)
{
var errorString = await response.Content.ReadAsStringAsync();
var errorData = JsonConvert.DeserializeObject<dynamic>(errorString);
return false;
}
var contentAsString = await response.Content.ReadAsStringAsync();
var responseData = JsonConvert.DeserializeObject<dynamic>(contentAsString);
var newAccessToken = (string)responseData.access_token;
var newRefreshToken = (string)responseData.refresh_token;
await TokenUtils.SetAccessToken(newAccessToken, mvcContext.HttpContext);
await TokenUtils.SetRefreshToken(newRefreshToken, mvcContext.HttpContext);
var result = await mvcContext.HttpContext.AuthenticateAsync();
if (result.Succeeded)
{
result.Properties.StoreTokens(new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = newAccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = newRefreshToken
}
});
return true;
}
else
{
return false;
}
}
I would like to know what is the good pratice to store (or replace the actual access token) with the .net core authentification. I'm sure I'm not doing it the right way. At the end I want to handle correctly my token and my refresh token for not ask the user to login again. Now with my solution my project will denied the access when it need to refresh the token and next request will be granted (because the cookie need to be resend from the user).
Thank to Ruard van Elburg I found the solution (here's the complete answer)
And that's what I used to replace my tokens:
// Save the information in the cookie
var info = await mvcContext.HttpContext.AuthenticateAsync("Cookies");
info.Properties.UpdateTokenValue("refresh_token", newRefreshToken);
info.Properties.UpdateTokenValue("access_token", newAccessToken);
info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
await mvcContext.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);

IdentityServer3 and external login through OpenIDConnect

In ASP.NET MVC app, I am trying to implement authentication against external OIDC service. For my testing I am using IdentityServer3 (https://identityserver.github.io/Documentation/) and public OIDC demo server: https://mitreid.org/
I cloned this sample from GitHub: https://github.com/IdentityServer/IdentityServer3.Samples/tree/master/source/MVC%20Authentication
Then added the following code to register the public OIDC server as external login provider:
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "<AuthTypeName>",
Authority = "https://mitreid.org/",
Caption = "MIT Test Server",
ClientId = "<Client Id>",
ClientSecret = "<Client Secret>",
RedirectUri = "https://localhost:44319/", //NOT SURE WHAT TO PUT HERE
ResponseType = "code",
Scope = "openid email profile",
SignInAsAuthenticationType = signInAsType
});
}
The code works, i get the option to login via external OIDC server. The browser redirects to the external server login page and when login and password is entered, the consent page is shown. However, after the browser navigates back to https://localhost:44319/ the user is not authenticated - User.Identity.IsAuthenticated is false.
Question: What should be correct value of RedirectUri property? Does OpenIdConnect middleware have capability to parse the authantication info passed in from external server or it must be coded manually? Is there any sample code how to do this?
I was studying the code and debugging quite a few hours (I am new to this) and I learned that:
This problem is related to OpenIdConnect OWIN Middleware implemented by Microsoft (https://github.com/aspnet/AspNetKatana/tree/dev/src/Microsoft.Owin.Security.OpenIdConnect).
The middleware from Microsoft expect that the OIDC server sends the message using HTTP POST, but the MIT server does HTTP GET
The middleware from Microsoft expect that there is id token along with code in the message obtained from OIDC server, but the MIT server sends only the code.
Looks like the RedirectUri can be any path under /identity because the middleware method AuthenticateCoreAsync() is hit on every request and it does compare request path to configured Options.CallbackPath (which is set from RedirectURI)
So I just had to implement the standard authorization code flow - exchange the code for id token, get claims, create authentication ticket and redirect to IdentityServer /identity/callback endpoint. When I've done this, everything started working. IdentityServer is awesome!
I inherited new set of classes from OpenIdConnect middleware and did override some methods. The key method is async Task<AuthenticationTicket> AuthenticateCoreAsync() in OpenIdConnectAuthenticationHandler. I pasted the code below in case it would help to someone.
public class CustomOidcHandler : OpenIdConnectAuthenticationHandler
{
private const string HandledResponse = "HandledResponse";
private readonly ILogger _logger;
private OpenIdConnectConfiguration _configuration;
public CustomOidcHandler(ILogger logger) : base(logger)
{
_logger = logger;
}
/// <summary>
/// Invoked to process incoming authentication messages.
/// </summary>
/// <returns>An <see cref="AuthenticationTicket"/> if successful.</returns>
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
// Allow login to be constrained to a specific path. Need to make this runtime configurable.
if (Options.CallbackPath.HasValue && Options.CallbackPath != (Request.PathBase + Request.Path))
return null;
OpenIdConnectMessage openIdConnectMessage = null;
if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
openIdConnectMessage = new OpenIdConnectMessage(Request.Query);
if (openIdConnectMessage == null)
return null;
ExceptionDispatchInfo authFailedEx = null;
try
{
return await CreateAuthenticationTicket(openIdConnectMessage).ConfigureAwait(false);
}
catch (Exception exception)
{
// We can't await inside a catch block, capture and handle outside.
authFailedEx = ExceptionDispatchInfo.Capture(exception);
}
if (authFailedEx != null)
{
_logger.WriteError("Exception occurred while processing message: ", authFailedEx.SourceException);
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification.
if (Options.RefreshOnIssuerKeyNotFound && authFailedEx.SourceException.GetType() == typeof(SecurityTokenSignatureKeyNotFoundException))
Options.ConfigurationManager.RequestRefresh();
var authenticationFailedNotification = new AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage,
Exception = authFailedEx.SourceException
};
await Options.Notifications.AuthenticationFailed(authenticationFailedNotification).ConfigureAwait(false);
if (authenticationFailedNotification.HandledResponse)
return GetHandledResponseTicket();
if (authenticationFailedNotification.Skipped)
return null;
authFailedEx.Throw();
}
return null;
}
private async Task<AuthenticationTicket> CreateAuthenticationTicket(OpenIdConnectMessage openIdConnectMessage)
{
var messageReceivedNotification =
new MessageReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage
};
await Options.Notifications.MessageReceived(messageReceivedNotification).ConfigureAwait(false);
if (messageReceivedNotification.HandledResponse)
{
return GetHandledResponseTicket();
}
if (messageReceivedNotification.Skipped)
{
return null;
}
// runtime always adds state, if we don't find it OR we failed to 'unprotect' it this is not a message we
// should process.
AuthenticationProperties properties = GetPropertiesFromState(openIdConnectMessage.State);
if (properties == null)
{
_logger.WriteWarning("The state field is missing or invalid.");
return null;
}
// devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users.
if (!string.IsNullOrWhiteSpace(openIdConnectMessage.Error))
{
throw new OpenIdConnectProtocolException(
string.Format(CultureInfo.InvariantCulture,
openIdConnectMessage.Error,
"Exception_OpenIdConnectMessageError", openIdConnectMessage.ErrorDescription ?? string.Empty,
openIdConnectMessage.ErrorUri ?? string.Empty));
}
// tokens.Item1 contains id token
// tokens.Item2 contains access token
Tuple<string, string> tokens = await GetTokens(openIdConnectMessage.Code, Options)
.ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken))
openIdConnectMessage.IdToken = tokens.Item1;
var securityTokenReceivedNotification =
new SecurityTokenReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context,
Options)
{
ProtocolMessage = openIdConnectMessage,
};
await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification).ConfigureAwait(false);
if (securityTokenReceivedNotification.HandledResponse)
return GetHandledResponseTicket();
if (securityTokenReceivedNotification.Skipped)
return null;
if (_configuration == null)
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.Request.CallCancelled)
.ConfigureAwait(false);
// Copy and augment to avoid cross request race conditions for updated configurations.
TokenValidationParameters tvp = Options.TokenValidationParameters.Clone();
IEnumerable<string> issuers = new[] {_configuration.Issuer};
tvp.ValidIssuers = tvp.ValidIssuers?.Concat(issuers) ?? issuers;
tvp.IssuerSigningTokens = tvp.IssuerSigningTokens?.Concat(_configuration.SigningTokens) ?? _configuration.SigningTokens;
SecurityToken validatedToken;
ClaimsPrincipal principal =
Options.SecurityTokenHandlers.ValidateToken(openIdConnectMessage.IdToken, tvp, out validatedToken);
ClaimsIdentity claimsIdentity = principal.Identity as ClaimsIdentity;
var claims = await GetClaims(tokens.Item2).ConfigureAwait(false);
AddClaim(claims, claimsIdentity, "sub", ClaimTypes.NameIdentifier, Options.AuthenticationType);
AddClaim(claims, claimsIdentity, "given_name", ClaimTypes.GivenName);
AddClaim(claims, claimsIdentity, "family_name", ClaimTypes.Surname);
AddClaim(claims, claimsIdentity, "preferred_username", ClaimTypes.Name);
AddClaim(claims, claimsIdentity, "email", ClaimTypes.Email);
// claims principal could have changed claim values, use bits received on wire for validation.
JwtSecurityToken jwt = validatedToken as JwtSecurityToken;
AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, properties);
if (Options.ProtocolValidator.RequireNonce)
{
if (String.IsNullOrWhiteSpace(openIdConnectMessage.Nonce))
openIdConnectMessage.Nonce = jwt.Payload.Nonce;
// deletes the nonce cookie
RetrieveNonce(openIdConnectMessage);
}
// remember 'session_state' and 'check_session_iframe'
if (!string.IsNullOrWhiteSpace(openIdConnectMessage.SessionState))
ticket.Properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = openIdConnectMessage.SessionState;
if (!string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe))
ticket.Properties.Dictionary[OpenIdConnectSessionProperties.CheckSessionIFrame] =
_configuration.CheckSessionIframe;
if (Options.UseTokenLifetime)
{
// Override any session persistence to match the token lifetime.
DateTime issued = jwt.ValidFrom;
if (issued != DateTime.MinValue)
{
ticket.Properties.IssuedUtc = issued.ToUniversalTime();
}
DateTime expires = jwt.ValidTo;
if (expires != DateTime.MinValue)
{
ticket.Properties.ExpiresUtc = expires.ToUniversalTime();
}
ticket.Properties.AllowRefresh = false;
}
var securityTokenValidatedNotification =
new SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context,
Options)
{
AuthenticationTicket = ticket,
ProtocolMessage = openIdConnectMessage,
};
await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification).ConfigureAwait(false);
if (securityTokenValidatedNotification.HandledResponse)
{
return GetHandledResponseTicket();
}
if (securityTokenValidatedNotification.Skipped)
{
return null;
}
// Flow possible changes
ticket = securityTokenValidatedNotification.AuthenticationTicket;
// there is no hash of the code (c_hash) in the jwt obtained from the server
// I don't know how to perform the validation using ProtocolValidator without the hash
// that is why the code below is commented
//var protocolValidationContext = new OpenIdConnectProtocolValidationContext
//{
// AuthorizationCode = openIdConnectMessage.Code,
// Nonce = nonce
//};
//Options.ProtocolValidator.Validate(jwt, protocolValidationContext);
if (openIdConnectMessage.Code != null)
{
var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options)
{
AuthenticationTicket = ticket,
Code = openIdConnectMessage.Code,
JwtSecurityToken = jwt,
ProtocolMessage = openIdConnectMessage,
RedirectUri =
ticket.Properties.Dictionary.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey)
? ticket.Properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey]
: string.Empty,
};
await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification)
.ConfigureAwait(false);
if (authorizationCodeReceivedNotification.HandledResponse)
{
return GetHandledResponseTicket();
}
if (authorizationCodeReceivedNotification.Skipped)
{
return null;
}
// Flow possible changes
ticket = authorizationCodeReceivedNotification.AuthenticationTicket;
}
return ticket;
}
private static void AddClaim(IEnumerable<Tuple<string, string>> claims, ClaimsIdentity claimsIdentity, string key, string claimType, string issuer = null)
{
string subject = claims
.Where(it => it.Item1 == key)
.Select(x => x.Item2).SingleOrDefault();
if (!string.IsNullOrWhiteSpace(subject))
claimsIdentity.AddClaim(
new System.Security.Claims.Claim(claimType, subject, ClaimValueTypes.String, issuer));
}
private async Task<Tuple<string, string>> GetTokens(string authorizationCode, OpenIdConnectAuthenticationOptions options)
{
// exchange authorization code at authorization server for an access and refresh token
Dictionary<string, string> post = null;
post = new Dictionary<string, string>
{
{"client_id", options.ClientId},
{"client_secret", options.ClientSecret},
{"grant_type", "authorization_code"},
{"code", authorizationCode},
{"redirect_uri", options.RedirectUri}
};
string content;
using (var client = new HttpClient())
{
var postContent = new FormUrlEncodedContent(post);
var response = await client.PostAsync(options.Authority.TrimEnd('/') + "/token", postContent)
.ConfigureAwait(false);
content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
// received tokens from authorization server
var json = JObject.Parse(content);
var accessToken = json["access_token"].ToString();
string idToken = null;
if (json["id_token"] != null)
idToken = json["id_token"].ToString();
return new Tuple<string, string>(idToken, accessToken);
}
private async Task<IEnumerable<Tuple<string, string>>> GetClaims(string accessToken)
{
string userInfoEndpoint = Options.Authority.TrimEnd('/') + "/userinfo";
var userInfoClient = new UserInfoClient(new Uri(userInfoEndpoint), accessToken);
var userInfoResponse = await userInfoClient.GetAsync().ConfigureAwait(false);
var claims = userInfoResponse.Claims;
return claims;
}
private static AuthenticationTicket GetHandledResponseTicket()
{
return new AuthenticationTicket(null, new AuthenticationProperties(new Dictionary<string, string>() { { HandledResponse, "true" } }));
}
private AuthenticationProperties GetPropertiesFromState(string state)
{
// assume a well formed query string: <a=b&>OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey=kasjd;fljasldkjflksdj<&c=d>
int startIndex = 0;
if (string.IsNullOrWhiteSpace(state) || (startIndex = state.IndexOf("OpenIdConnect.AuthenticationProperties", StringComparison.Ordinal)) == -1)
{
return null;
}
int authenticationIndex = startIndex + "OpenIdConnect.AuthenticationProperties".Length;
if (authenticationIndex == -1 || authenticationIndex == state.Length || state[authenticationIndex] != '=')
{
return null;
}
// scan rest of string looking for '&'
authenticationIndex++;
int endIndex = state.Substring(authenticationIndex, state.Length - authenticationIndex).IndexOf("&", StringComparison.Ordinal);
// -1 => no other parameters are after the AuthenticationPropertiesKey
if (endIndex == -1)
{
return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex).Replace('+', ' ')));
}
else
{
return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex, endIndex).Replace('+', ' ')));
}
}
}
public static class CustomOidcAuthenticationExtensions
{
/// <summary>
/// Adds the <see cref="OpenIdConnectAuthenticationMiddleware"/> into the OWIN runtime.
/// </summary>
/// <param name="app">The <see cref="IAppBuilder"/> passed to the configuration method</param>
/// <param name="openIdConnectOptions">A <see cref="OpenIdConnectAuthenticationOptions"/> contains settings for obtaining identities using the OpenIdConnect protocol.</param>
/// <returns>The updated <see cref="IAppBuilder"/></returns>
public static IAppBuilder UseCustomOidcAuthentication(this IAppBuilder app, OpenIdConnectAuthenticationOptions openIdConnectOptions)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
if (openIdConnectOptions == null)
throw new ArgumentNullException(nameof(openIdConnectOptions));
return app.Use(typeof(CustomOidcMiddleware), app, openIdConnectOptions);
}
}
and in Startup.cs
public class Startup
{
....
public void Configuration(IAppBuilder app)
{
....
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseCustomOidcAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "<name>",
Authority = "<OIDC server url>",
Caption = "<caption>",
ClientId = "<client id>",
ClientSecret = "<client secret>",
// might be https://localhost:44319/identity/<anything>
RedirectUri = "https://localhost:44319/identity/signin-customoidc",
ResponseType = "code",
Scope = "openid email profile address phone",
SignInAsAuthenticationType = signInAsType
}
);
}
....
}
....
}

Categories