AccessToken from Azure ADB2C in MVC5 application - c#

We are working on a MVC5 web application, that uses OpenIdConnect to authenticate to Azure AD B2C. When a user has authenticated, we would like to be able to acquire accesstokens from Azure AD B2C, in order to use them our API.
This is our Startup.cs-equivalent code:
protected override void ProcessCore(IdentityProvidersArgs args)
{
Assert.ArgumentNotNull(args, nameof(args));
List<B2CConfig> ssoSettings = _ssoConfigurationRepository.GetAllSettings();
foreach (var config in ssoSettings)
{
args.App.UseOpenIdConnectAuthentication(CreateOptionsFromSiteConfig(config));
}
}
private OpenIdConnectAuthenticationOptions CreateOptionsFromSiteConfig(B2CConfig config)
{
OpenIdConnectAuthenticationOptions options = new OpenIdConnectAuthenticationOptions();
options.MetadataAddress = string.Format(_aadInstance, _tenant, config.Policy);
options.AuthenticationType = config.Policy;
options.AuthenticationMode = AuthenticationMode.Passive;
options.RedirectUri = config.AzureReplyUri;
options.PostLogoutRedirectUri = config.LogoutRedirectUri;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "emails"
};
var identityProvider = GetIdentityProvider();
options.Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthenticationFailed = AuthenticationFailed,
RedirectToIdentityProvider = notification =>
{
return Task.FromResult(notification.ProtocolMessage.UiLocales = config.UiLocale ?? string.Empty);
},
SecurityTokenValidated = notification =>
{
notification.AuthenticationTicket.Identity.AddClaim(new Claim("idp", "azureadb2c"));
// transform all claims
ClaimsIdentity identity = notification.AuthenticationTicket.Identity;
notification.AuthenticationTicket.Identity.ApplyClaimsTransformations(new TransformationContext(FederatedAuthenticationConfiguration, identityProvider));
return Task.CompletedTask;
}
};
options.ClientId = config.ClientId;
options.Scope = "openid";
options.ResponseType = "id_token";
return options;
}
private Task AuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
{
SsoLogger.Warn("User triggered reset password");
notification.Response.Redirect(SsoConfiguration.Routes.ResetPassword);
}
else if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
SsoLogger.Warn("AuthenticationFailed", notification.Exception);
notification.Response.Redirect(SsoConfiguration.Routes.LoginError);
}
return Task.FromResult(0);
}
In Asp.Net core it seems like you would call GetTokenAsync on the HttpContext, but that extensionmethod is not available in .NET 4.72.
Can anyone help figuring out, how to retrieve an accesstoken from AzureAD B2C, that can be used in the calls to our WebApi? Or can I just store the accesstoken I get from the SecurityTokenValidated event and use that for all API requests?

This is a possible solution: Is it safe to store an access_token in a user claim for authorization?
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = notification =>
{
var identity = notification.AuthenticationTicket.Identity;
identity.AddClaim(claim: new Claim(type: "auth_token", value:
notification.ProtocolMessage.AccessToken));
return Task.CompletedTask;
}
}
If anyone has a better approach I will gladly accept another answer.

Related

AcquireTokenByAuthorizationCode throw new exception in single tenant application using ASP.NET MVC using Azure Active Directory

I tried this tutorial because I want to use the Microsoft Graph API to create massive teams in Microsoft Teams.
The only difference with the tutorial is that I used the next choice in Authentication section of Azure AD admin center:
"Accounts in this organizational directory only (myuniversity only - Single tenant)"
Because of this I changed my code to use endpoint for single tenant
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
//Authority = "https://login.microsoftonline.com/common/v2.0",//
Authority = "https://login.microsoftonline.com/{tenantid}/v2.0",
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
// For demo purposes only, see below
ValidateIssuer = false
// In a real multi-tenant app, you would add logic to determine whether the
// issuer was from an authorized tenant
//ValidateIssuer = true,
//IssuerValidator = (issuer, token, tvp) =>
//{
// if (MyCustomTenantValidation(issuer))
// {
// return issuer;
// }
// else
// {
// throw new SecurityTokenInvalidIssuerException("Invalid issuer");
// }
//}
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
After authentication of the user the code I run to OnAuthorizationCodeReceivedAsync method but got an exception in AcquireTokenByAuthorizationCode method
Here is the method
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.Build();
var accounts = await idClient.GetAccountsAsync();
string message;
string debug;
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(scopes, notification.Code).ExecuteAsync();
message = "Access token retrieved.";
debug = result.AccessToken;
}
catch (MsalException ex)
{
message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
debug = ex.Message;
}
var queryString = $"message={message}&debug={debug}";
if (queryString.Length > 2048)
{
queryString = queryString.Substring(0, 2040) + "...";
}
notification.HandleResponse();
notification.Response.Redirect($"/Home/Error?{queryString}");
}
The exception is:
AcquireTokenByAuthorizationCodeAsync threw an exception
AADSTS50194: Application 'application id'(ASP.NET Graph Tutorial) is
not configured as a multi-tenant application. Usage of the /common
endpoint is not supported for such applications created after
'10/15/2018'. Use a tenant-specific endpoint or configure the
application to be multi-tenant. Trace ID:
5f0fbf2e-5d63-40d4-a833-ca8627a02d00
Correlation ID: 3ec4ec7b-0c86-4e2b-a053-9823f977499d Timestamp:
2021-02-16 20:21:03Z
I want to use single-tenant authentication for my organization only
If you want to require AD token from one specific tenant with MSAL.NET, you can tell the SDK from which tenant to obtain the token by mentioning the specific Authority. For more details, please refer to here.
For example
private static string appId = ConfigurationManager.AppSettings["ida:AppId"];
private static string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private static string graphScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/<your tenant id>/v2.0",
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
// For demo purposes only, see below
ValidateIssuer = false
// In a real multi-tenant app, you would add logic to determine whether the
// issuer was from an authorized tenant
//ValidateIssuer = true,
//IssuerValidator = (issuer, token, tvp) =>
//{
// if (MyCustomTenantValidation(issuer))
// {
// return issuer;
// }
// else
// {
// throw new SecurityTokenInvalidIssuerException("Invalid issuer");
// }
//}
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.WithAuthority("https://login.microsoftonline.com/<your tenant id>")
.Build();
string message;
string debug;
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
message = "Access token retrieved.";
debug = result.AccessToken;
}
catch (MsalException ex)
{
message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
debug = ex.Message;
}
var queryString = $"message={message}&debug={debug}";
if (queryString.Length > 2048)
{
queryString = queryString.Substring(0, 2040) + "...";
}
notification.HandleResponse();
notification.Response.Redirect($"/Home/Error?{queryString}");
}
private Task OnAuthenticationFailedAsync(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
string redirect = $"/Home/Error?message={notification.Exception.Message}";
if (notification.ProtocolMessage != null && !string.IsNullOrEmpty(notification.ProtocolMessage.ErrorDescription))
{
redirect += $"&debug={notification.ProtocolMessage.ErrorDescription}";
}
notification.Response.Redirect(redirect);
return Task.FromResult(0);
}

Cookies are not refreshed in .NET Core 3.1

I have an MVC .NET Core 3.1 application that uses Open ID connect for authentication and stores identity & tokens in cookies. The tokens need to be refreshed as they are used in some API requests our application does. I subscribe to ValidatePrincipal event and refresh the tokens there. The request goes OK, but cookies are not updated for some reason.
Startup.cs:
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.OnAppendCookie = (e) =>
{
e.CookieOptions.Domain = <some domain>
};
});
...
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.Cookie.Domain = <some domain>;
options.Cookie.IsEssential = true;
options.Cookie.Name = <some name>
options.EventsType = typeof(CookieAuthEvents);
})
CookieAuthEvents.cs (constructor, member declarations and logging are omitted):
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
if (context.Principal.Identity.IsAuthenticated)
{
var now = DateTime.UtcNow;
var tokenExpiration = context.Properties.GetTokenExpiration();
if (now > tokenExpiration)
{
await UpdateCookies(context);
}
}
}
private async Task UpdateCookies(CookieValidatePrincipalContext context)
{
var refreshToken = context.Properties.GetTokenValue(OpenIdConnectGrantTypes.RefreshToken);
if (String.IsNullOrEmpty(refreshToken))
{
return;
}
var response = await GetTokenClient().RequestRefreshTokenAsync(refreshToken);
if (!response.IsError)
{
WriteCookies(context, response);
}
else
{
context.RejectPrincipal();
}
}
private void WriteCookies(CookieValidatePrincipalContext context, TokenResponse response)
{
var tokens = new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.IdToken,
Value = response.IdentityToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = response.AccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = response.RefreshToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.TokenType,
Value = response.TokenType
}
};
var expiresAt = DateTime.UtcNow.AddSeconds(response.ExpiresIn);
tokens.Add(new AuthenticationToken
{
Name = "expires_at",
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
});
var newPrincipal = GetNewPrincipal(context);
context.ReplacePrincipal(newPrincipal);
context.Properties.StoreTokens(tokens);
context.ShouldRenew = true;
}
private static ClaimsPrincipal GetNewPrincipal(CookieValidatePrincipalContext context)
{
var claims = context.Principal.Claims.Select(c => new Claim(c.Type, c.Value)).ToList();
var authTimeClaim = claims.FirstOrDefault(claim => claim.Type.Same("auth_time"));
if (authTimeClaim != null)
{
claims.Remove(authTimeClaim);
}
claims.Add(new Claim("auth_time", DateTime.UtcNow.UnixTimestamp().ToString()));
return new ClaimsPrincipal(new ClaimsIdentity(claims, context.Principal.Identity.AuthenticationType));
}
The main problem is this works perfectly fine locally. All the calls are fine, cookies are refreshed correctly. But when the app is run from dev machine (we host it in Azure App Service) RequestRefreshTokenAsync call is successful, but the cookies are not updated therefore all the next calls are made with an old tokens leading to 400 invalid_grant error. So basically what I see in logs is:
RequestRefreshTokenAsync successful. The old refresh_token is used to get a new one
ValidatePrincipal successful. Here the cookies should be rewritten (including a new refresh_token)
Next request - RequestRefreshTokenAsync failed. The old refresh_token is used (even though it's invalid).
I tried playing with cookies configurations and placing semaphores and locks inside ValidatePrincipal method but none of it worked.
Does anyone have an idea what can cause that?

OWIN OpenId Authentication - Active Session after logout

I have implemented the mix of ASP.Net Cookie Authentication & OWIN OpenId authentication in my application. I am trying to fix a security flaw where the session is not invalidating even after logout.
Middleware Implementation:
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
}
}
);
Log Out Code (Based on user type):
HttpContext.GetOwinContext().Authentication.SignOut(
OpenIdConnectAuthenticationDefaults.AuthenticationType,
CookieAuthenticationDefaults.AuthenticationType);
HttpContext.GetOwinContext().Authentication.SignOut(
CookieAuthenticationDefaults.AuthenticationType);
I am capturing the traffic in Fiddler and clicking sign-out from web page. When I try to re-issue the request from Fiddler, it's completing successfully and in HttpModule, the Application.User.Identity.IsAuthenticated is True
I have a couple of questions:-
Is this s Cookie replay attack?
What I am doing wrong, if not I will
have to fix it by some hack, like storing a cookie in the cache and
comparing it?
When signing out from your application you have to sign out from the Identity server too. Otherwise, your app will be redirected to identity server, get re-authenticated and log back in again. Check the following code snippet under notifications:
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
}
);
You will find some examples of OWIN Middleware setup (not a direct answer to your question though) here
Not sure if this answer can help other but, here the link has some more information on how to setup openId with MVC application.
Changing the middleware configuration
Add OpenId & Cookies authentication middleware in startup.cs file. set ResponseType to Id_token to have openId logout too work.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieHttpOnly = true,
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
CookieName = "AppCookies",
ExpireTimeSpan = TimeSpan.FromMinutes(30),
SlidingExpiration = true
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44319/identity",
ClientId = "mvc",
Scope = "openid profile roles",
RedirectUri = "https://localhost:44319/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
// we want to keep first name, last name, subject and roles
var givenName = id.FindFirst(Constants.ClaimTypes.GivenName);
var familyName = id.FindFirst(Constants.ClaimTypes.FamilyName);
var sub = id.FindFirst(Constants.ClaimTypes.Subject);
var roles = id.FindAll(Constants.ClaimTypes.Role);
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
id.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
nid.AddClaim(givenName);
nid.AddClaim(familyName);
nid.AddClaim(sub);
nid.AddClaims(roles);
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
return Task.FromResult(0);
},
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if ((int)n.ProtocolMessage.RequestType == (int)OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst(Startup.IdToken);
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
});
Adding Logout
Adding logout is easy, simply add a new action that calls the Signout method in the Katana authentication manager:
public ActionResult Logout()
{
Session.Abandon();
// clear session cookie (not necessary for your current problem but i would recommend you do it anyway)
HttpCookie cookie2 = new HttpCookie("ASP.NET_SessionId", "");
cookie2.HttpOnly = true;
cookie2.Expires = DateTime.Now.AddYears(-1);
Response.Cookies.Add(cookie2);
// clear site cookie
var siteCookie = new HttpCookie("AppCookies", "");
siteCookie.HttpOnly = true;
siteCookie.Expires = DateTime.Now.AddYears(-1);
Response.Cookies.Add(siteCookie);
Request.GetOwinContext().Authentication.SignOut();
return Redirect("/");
}

Manually generate IdentityServer4 reference token and save to PersistedGrants table

I have learnt IdentityServer4 for a week and successfully implemented a simple authentication flow with ResourceOwnedPassword flow.
Now, I'm implementing Google authentication with IdentityServer4 by following this tutorial
This is what I am doing:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//...
const string connectionString = #"Data Source=.\SQLEXPRESS;database=IdentityServer4.Quickstart.EntityFramework-2.0.0;trusted_connection=yes;";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddProfileService<IdentityServerProfileService>()
.AddResourceOwnerValidator<IdentityResourceOwnerPasswordValidator>()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
{
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
};
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
});
// Add jwt validation.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddIdentityServerAuthentication(options =>
{
// base-address of your identityserver
options.Authority = "https://localhost:44386";
options.ClaimsIssuer = "https://localhost:44386";
// name of the API resource
options.ApiName = "api1";
options.ApiSecret = "secret";
options.RequireHttpsMetadata = false;
options.SupportedTokens = SupportedTokens.Reference;
});
//...
}
** Google controller (Which is for handling returned token from Google **
public class GLoginController : Controller
{
#region Properties
private readonly IPersistedGrantStore _persistedGrantStore;
private readonly IUserFactory _userFactory;
private readonly IBaseTimeService _baseTimeService;
private readonly ITokenCreationService _tokenCreationService;
private readonly IReferenceTokenStore _referenceTokenStore;
private readonly IBaseEncryptionService _baseEncryptionService;
#endregion
#region Constructor
public GLoginController(IPersistedGrantStore persistedGrantStore,
IBaseTimeService basetimeService,
ITokenCreationService tokenCreationService,
IReferenceTokenStore referenceTokenStore,
IBaseEncryptionService baseEncryptionService,
IUserFactory userFactory)
{
_persistedGrantStore = persistedGrantStore;
_baseTimeService = basetimeService;
_userFactory = userFactory;
_tokenCreationService = tokenCreationService;
_referenceTokenStore = referenceTokenStore;
_baseEncryptionService = baseEncryptionService;
}
#endregion
#region Methods
[HttpGet("login")]
[AllowAnonymous]
public IActionResult Login()
{
var authenticationProperties = new AuthenticationProperties
{
RedirectUri = "/api/google/handle-external-login"
};
return Challenge(authenticationProperties, "Google");
}
[HttpGet("handle-external-login")]
//[Authorize("ExternalCookie")]
[AllowAnonymous]
public async Task<IActionResult> HandleExternalLogin()
{
//Here we can retrieve the claims
var authenticationResult = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
var principal = authenticationResult.Principal;
var emailAddress = principal.FindFirst(ClaimTypes.Email)?.Value;
if (string.IsNullOrEmpty(emailAddress))
return NotFound(new ApiMessageViewModel("Email is not found"));
// Find user by using username.
var loadUserConditions = new LoadUserModel();
loadUserConditions.Usernames = new HashSet<string> { emailAddress };
loadUserConditions.Pagination = new PaginationValueObject(1, 1);
// Find users asynchronously.
var loadUsersResult = await _userFactory.FindUsersAsync(loadUserConditions);
var user = loadUsersResult.FirstOrDefault();
// User is not defined.
if (user == null)
{
user = new User(Guid.NewGuid(), emailAddress);
user.Email = emailAddress;
user.HashedPassword = _baseEncryptionService.Md5Hash("abcde12345-");
user.JoinedTime = _baseTimeService.DateTimeUtcToUnix(DateTime.UtcNow);
user.Kind = UserKinds.Google;
user.Status = UserStatuses.Active;
//await _userFactory.AddUserAsync(user);
}
else
{
// User is not google account.
if (user.Kind != UserKinds.Google)
return Forbid("User is not allowed to access system.");
}
var token = new Token(IdentityServerConstants.TokenTypes.IdentityToken);
var userCredential = new UserCredential(user);
token.Claims = userCredential.GetClaims();
token.AccessTokenType = AccessTokenType.Reference;
token.ClientId = "ro.client";
token.CreationTime = DateTime.UtcNow;
token.Audiences = new[] {"api1"};
token.Lifetime = 3600;
return Ok();
}
#endregion
}
Everything is fine, I can get claims returned from Google OAuth2, find users in database using Google email address and register them if they dont have any acounts.
My question is: How can I use Google OAuth2 claims that I receive in HandleExternalLogin method to generate a Reference Token, save it to PersistedGrants table and return to client.
This means when user access https://localhost:44386/api/google/login, after being redirected to Google consent screen, they can receive access_token, refresh_token that has been generated by IdentityServer4.
Thank you,
In IdentityServer the kind (jwt of reference) of the token is
configurable for each client (application), requested the token.
AccessTokenType.Reference is valid for TokenTypes.AccessToken not
TokenTypes.IdentityToken as in your snippet.
In general it would be simpler to follow the original quickstart and then extend the generic code up to your needs. What I can see now in the snippet above is just your specific stuff and not the default part, responsible for creating the IdSrv session and redirecting back to the client.
If you still like to create a token manually:
inject ITokenService into your controller.
fix the error I mentioned above: TokenTypes.AccessToken instead of TokenTypes.IdentityToken
call var tokenHandle = await TokenService.CreateAccessTokenAsync(token);
tokenHandle is a key in PersistedGrantStore

How to add custom headers to an open id connect authentication request in OWIN middleware

I have a system where I have an MVC website calling a web api. I have used OAUTH and OPENID Connect for my authentication/authorization. I have setup an identity server in another web api project using Thinktecture's IdentityServer3. In the MVC project I am doing the redirect to the identity server in the OWIN Startup class. This is all pretty standard stuff and is working fine.
I have now been asked to put the web api and the identity server behind Azure API Management. This also, is fine as all I need to do from my MVC project is add my subscription key from API management as a header ("Ocp-Apim-Subscription-Key") to any request to the web api. So far so good.
The problem is I now need to add this header to any requests to the identity server and I can't work out how, other than writing my own middleware. My Startup class looks like this:
public class Startup
{
public void Configuration(IAppBuilder app)
{
DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(IocHelper.GetContainer()));
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigurationHelper.GetClientId(),
Authority = ConfigurationHelper.GetSecurityTokenServiceUrl(),
RedirectUri = ConfigurationHelper.GetPortalHomePageUrl(),
PostLogoutRedirectUri = ConfigurationHelper.GetPortalHomePageUrl(),
ResponseType = "code id_token",
Scope = "openid profile public_api",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(ConfigurationHelper.GetTokenEndpointUrl(), ConfigurationHelper.GetClientId(), ConfigurationHelper.GetClientSecret());
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(ConfigurationHelper.GetUserInfoUrl());
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
if (userInfoResponse.IsError)
{
throw new Exception(userInfoResponse.Error);
}
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
foreach (var c in userInfoResponse.Claims)
{
id.AddClaim(new Claim(c.Type, c.Value));
}
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn * 2).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
var claimsIdentity = new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role");
//claimsIdentity.IsAuthenticated = true;
n.AuthenticationTicket = new AuthenticationTicket(claimsIdentity, n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.LogoutRequest)
{
return Task.FromResult(0);
}
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
// DOESN'T WORK
n.OwinContext.Request.Headers.Append("Ocp-Apim-Subscription-Key", "MY KEY GOES HERE");
// ALSO DOESN'T WORK
n.Request.Headers.Append("Ocp-Apim-Subscription-Key", "MY KEY GOES HERE");
return Task.FromResult(0);
}
}
});
}
}
Is there a way to hi-jack the request to the identity server and add my own header in there?

Categories

Resources