I have a Asp.NET MVC / WebAPI project with an embedded IdentityServer3.
I want both MVC and WebAPI to be protected by the IdentityServer. So I have used Authorize attribute on both MVC controllers and API controllers.
When surfing to my test page (which is protected) I get redirected to the IdentityServer login page. I enter my username and password and get authenticated and redirected back.
On the page I have a button that triggers a GET from javascript, with my access token in the authorization header, to my protected API. But here it fails with a 401 Unauthorized.
I get the access token to javascript by rendering it to the page with Razor.
I have 1 client in the IdentityServer set to hybrid flow. MVC uses cookies, while the API uses bearer tokens.
On my API HttpConfiguration I have set SuppressDefaultHostAuthentication. If I remove that line everything works, but then it uses cookies for the API which I don't want.
I use only HTTP and RequireSsl=false for now to avoid potential certificate problems.
I have tried for days to get this to work but I'm getting nowhere.
I don't even know how to debug this to get to cause of the 401.
By now I recognize just about every page that google suggests when I search for help.
Any ideas what it could be or how to debug this?
Here is my Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Trace()
.CreateLogger();
// MVC client
string authBaseAddress = "http://localhost:50319/identity";
string tokenEndpoint = authBaseAddress + "/connect/token";
string userInfoEndpoint = authBaseAddress + "/connect/userinfo";
string redirectUri = "http://localhost:50319/";
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "hybrid_clients",
Authority = authBaseAddress,
RedirectUri = redirectUri,
ResponseType = "code id_token token",
Scope = "openid profile roles sampleApi offline_access",
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(
tokenEndpoint,
"hybrid_clients",
"secret");
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(
new Uri(userInfoEndpoint),
tokenResponse.AccessToken);
var userInfoResponse = await userInfoClient.GetAsync();
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
// Attach the id_token for the logout roundtrip to IdentityServer
RedirectToIdentityProvider = n =>
{
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);
}
}
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
// web api
app.Map("/api", a =>
{
var config = new HttpConfiguration();
a.UseCors(CorsOptions.AllowAll);
a.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
//AuthenticationMode = AuthenticationMode.Active,
Authority = authBaseAddress,
RequiredScopes = new[] { "sampleApi" },
DelayLoadMetadata = true
});
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.MapHttpAttributeRoutes();
a.UseWebApi(config);
});
// Identity server
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseCors(CorsOptions.AllowAll);
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get()),
AuthenticationOptions = new IdentityServer3.Core.Configuration.AuthenticationOptions()
{
EnablePostSignOutAutoRedirect = true // Automatically redirects back to the client on signout
},
RequireSsl = false,
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(#"{0}\bin\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
My client
new Client
{
Enabled = true,
ClientName = "Hybrid Clients",
ClientId = "hybrid_clients",
Flow = Flows.Hybrid,
//AllowAccessTokensViaBrowser = false,
RedirectUris = new List<string>
{
"http://localhost:50319/"
},
PostLogoutRedirectUris = new List<string>
{
"http://localhost:50319/"
},
AllowedScopes = new List<string>
{
"openid",
"profile",
"email",
"roles",
"address",
"all_claims",
"sampleApi",
"offline_access"
},
ClientSecrets = new List<Secret>
{
new Secret("secret".Sha256())
},
AccessTokenType = AccessTokenType.Reference,
LogoutSessionRequired = true
},
My scopes
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.Profile,
StandardScopes.Email,
StandardScopes.Address,
StandardScopes.OfflineAccess,
StandardScopes.RolesAlwaysInclude,
StandardScopes.AllClaims,
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
DisplayName = "Sample API",
Name = "sampleApi",
Description = "Access to a sample API",
Type = ScopeType.Resource
}
};
return scopes;
}
}
My API
[Authorize]
public class SecuredApiController : ApiController
{
public IHttpActionResult Get()
{
var user = User as ClaimsPrincipal;
var claims = from c in user.Claims
select new
{
type = c.Type,
value = c.Value
};
return Json(claims);
}
}
Part of my Razor view
<button data-bind="click:callApi">Call API</button>
<span data-bind="text:apiResult"></span>
<script>
$(function() {
ko.myViewModel = new ClientAppViewModel('#ViewData["access_token"]');
ko.applyBindings(ko.myViewModel);
});
</script>
My JavaScript (KnockoutJS) that calls SecuredApi
function ClientAppViewModel(accessToken) {
var self = this;
self.accessToken = accessToken;
self.apiResult = ko.observable('empty');
self.callApi = function () {
console.log('CallApi');
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:50319/api/SecuredApi");
xhr.onload = function () {
self.apiResult(JSON.stringify(JSON.parse(xhr.response), null, 2));
};
xhr.setRequestHeader("Authorization", "Bearer " + self.accessToken);
xhr.send();
}
}
The Katana logs for the API are what you want. With these you will see why the API is returning a 401.
You can access these using the Microsoft.Owin log source (see Katana Documentation)
Related
I am using IdentityServer4 to get my OpenId tokens for my web app.
But my web app uses Owin for security. I cannot find any example samples of where this is done.
So this code works;
In my client;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using SIR.API.Caller.Helpers;
namespace SIR.API.Caller
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = Settings.SignInAsAuthenticationType // "Cookies";
});
app.UseOpenIdConnectAuthentication(openIdConnectOptions: new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
Authority = Settings.AuthorityUrl1, //ID Server, "https://localhost:44314/"; https://localhost:44307/
ClientId = Settings.ClientId, // "SIR"
Scope = Settings.Scope, // "openid profile";
ResponseType = Settings.ResponseType, // "id_token code";
SignInAsAuthenticationType = Settings.SignInAsAuthenticationType,
//--------------------------------------// "Cookies";
RedirectUri = Settings.RedirectUri, // URL of website, http://localhost:50000/signin-oidc;
//RedirectUri = Settings.RedirectUri1, // URL of website, http://localhost:53200/signin-oidc;
RequireHttpsMetadata = Settings.RequireHttpsMetadata,
//--------------------------------------// true
ClientSecret = "secret"
});
app.Use(async (ctx, next) =>
{
var message = ctx.Authentication.User.Identity.IsAuthenticated
? $"User: {ctx.Authentication.User.Identity.Name}"
: "User Not Authenticated";
await next();
});
}
}
}
In my ID 4 server the startup is;
using System.Security.Cryptography.X509Certificates;
using IdentityServer4;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Mulalley.IdentityServer4.Helpers;
using QuickstartIdentityServer;
namespace Mulalley.IdentityServer4
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
//.AddDeveloperSigningCredential()
.AddSigningCredential(new X509Certificate2(Settings.CertPath, Settings.Password))
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to http://localhost:port/signin-google
options.ClientId = "copy client ID from Google here";
options.ClientSecret = "copy client secret from Google here";
})
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "implicit";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
}
and the Config.cs is;
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System.Collections.Generic;
using System.Security.Claims;
namespace QuickstartIdentityServer
{
public class Config
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("SIR", "Service Inspection Report")
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
var baseUri = "http://localhost:53200/";
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "SIR" },
AlwaysIncludeUserClaimsInIdToken = true
},
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "SIR" },
AlwaysIncludeUserClaimsInIdToken = true
},
// OpenID Connect hybrid flow and client credentials client (MVC)
new Client
{
ClientId = "SIR",
ClientName = "SIR",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { baseUri + "signin-oidc" },
PostLogoutRedirectUris = { baseUri + "signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"SIR"
},
AllowOfflineAccess = true,
AlwaysIncludeUserClaimsInIdToken = true
}
};
}
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Alice"),
new Claim("website", "https://alice.com")
}
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Bob"),
new Claim("website", "https://bob.com")
}
}
};
}
}
}
And the question is how do I get the token in C# so I can have a look at it? And how do I set the GetClaimsFromUserInfoEndpoint = true which is not available in OWIN?
Are there any code samples I can look at with OWIN access to ID4?
EDIT: This code appears to be what I need: https://identitymodel.readthedocs.io/en/latest/client/token.html
However I put the code in and I get an access token which when I put it in jwt.io I get a message "Invalid Signature".
private static async Task<TokenResponse> GetClientCredentialsTokenResponse()
{
var client = new HttpClient();
var tokenRequest = new ClientCredentialsTokenRequest
{
Address = Settings.AuthorityUrl1 + "connect/token",
ClientId = Settings.ClientId,
ClientSecret = "secret",
Scope = "SIR"
};
return await client.RequestClientCredentialsTokenAsync(tokenRequest);
}
private static async Task<TokenResponse> GetTokenResponse()
{
var client = new HttpClient();
var tokenRequest = new TokenRequest
{
Address = Settings.AuthorityUrl1 + "connect/token",
GrantType = GrantType.ClientCredentials,
ClientId = Settings.ClientId,
ClientSecret = "secret",
Parameters =
{
{"scope", "SIR"}
}
};
return await client.RequestTokenAsync(tokenRequest);
}
private static void ThrowResponseException(TokenResponse response)
{
const string message = "Problem accessing the UserInfo endpoint";
if (response.Exception == null)
{
throw new Exception($"{message}: {response.Error}. {response.ErrorDescription}.");
}
throw new Exception(message, response.Exception);
}
}
}
When I run my project and try to login to get an access token, I get an Unauthorized_Client error on the browser or when testing with Postman. I'm fairly new to IdentityServer. This is my Configuration:
Config.cs
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
// custom identity resource with some consolidated claims
new IdentityResource("custom.profile", new[] { JwtClaimTypes.Name, JwtClaimTypes.Email, "location" })
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
//new ApiResource("NuB.hAPI", "Hospital API")
new ApiResource
{
Name = "NuB.HospitalSearch",
ApiSecrets = { new Secret("F621F470-9731-4A25-80EF-67A6F7C5F4B8".Sha256()) },
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Email,
JwtClaimTypes.PhoneNumber,
JwtClaimTypes.Gender,
"NuB.HospitalSearch"
},
Scopes =
{
new Scope
{
Name = "NuB.HospitalSearch",
DisplayName = "Full access to Hospital Search App"
},
new Scope
{
Name = "openid",
DisplayName = "Read only access to Hospital Search App"
}
}
}
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "mvcWeb",
ClientName = "MVC Web Client",
AllowedGrantTypes = GrantTypes.Implicit,
AccessTokenType = AccessTokenType.Jwt,
RequireConsent = true,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"NuB.HospitalSearch"
},
AllowOfflineAccess = true
}
};
}
I am using ASP.NET Identity with EntityFrameworkCore with IdentityServer4 to do this. It may by a simple problem but kindly point me to the right direction.
Unauthorized_Client means that your client is trying to authencate to the Ids with a client that does not exist. You apear to have created a client called mvcWeb
That means that your client will need to use a client id of mvcWeb your client code should probably look something like this. Note options.ClientId = "mvcWeb";
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = settingsSetup.Authority;
options.RequireHttpsMetadata = false;
options.ClientId = "mvcWeb";
options.ClientSecret = "secret";
options.ResponseType = oidcConstants.ResponseTypes.CodeIdTokenToken;
options.Scope.Add("openid");
options.Scope.Add("profile");
});
I have a working identity provider. The client I have designed to authenticate against it is a single project combining MVC and web API. The initial authentication is done me the MVC. If the access token becomes invalid it refreshes as expected.
MVC side:
public partial class Startup {
public void ConfigureAuth(IAppBuilder app)
{
//AntiForgeryConfig.UniqueClaimTypeIdentifier = "sub";
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieName = "CookieName",
ReturnUrlParameter = "/Dashboard",
LogoutPath = new PathString("/"),
});
app.UseOpenIdConnectAuthentication(GetOpenIdConnectAuthenticationOptions());
}
private OpenIdConnectAuthenticationOptions GetOpenIdConnectAuthenticationOptions()
{
var options = new OpenIdConnectAuthenticationOptions
{
ClientId = "client.id",
Authority = AuthorityUrl,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = RedirectUri,
ResponseType = "code id_token",
Scope = "openid profile email offline_access roles company utc_offset service_api",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = GetOpenIdConnectAuthenticationNotifications()
};
return options;
}
private OpenIdConnectAuthenticationNotifications GetOpenIdConnectAuthenticationNotifications()
{
var container = UnityLazyInit.Container;
var authorizationProvider = container.Resolve<AuthorizationProvider>();
var notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
authorizationProvider.Authority = Authority;
authorizationProvider.LoginMethod = LoginMethod;
var tokenResponse = await authorizationProvider.GetAccessAndRefreshTokens(n);
var userInfoClaims = await authorizationProvider.GetUserInfoClaims(tokenResponse);
userInfoClaims = authorizationProvider.TransformUserInfoClaims(userInfoClaims);
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoClaims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
var user = authorizationProvider.GetUser(id);
var applicationClaims = authorizationProvider.GetApplicationClaims(user);
id.AddClaims(applicationClaims);
var permisionClaims = authorizationProvider.GetPermisionClaims(user);
id.AddClaims(permisionClaims);
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
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);
}
};
return notifications;
}
}
The presentation layer (browser side) leverages angulerjs however I have not incorporated any support for authentication. I am relying on the MVC.
When you presentation layer makes calls to the API it automatically validates against the access token retrieved by the MVC but if it expires it is unable to refresh the access token. It also doesn’t return unauthorized. It appears to be trying to refresh but fails. The presentation receives the HTML for the error page of the identity provider when the api calls attempt to refresh the token.
How do I fix this? It seems to me is supposed to authenticate and refresh automatically for the MVC and the API when they are combined but this is not working for me.
note to clarify the start up configuration above is shared but the MVC and the API.
new Client
{
ClientName = "MVC Client",
ClientId = "client.id",
ClientSecrets = new List<Secret> {
new Secret("secret".Sha256())
},
Flow = Flows.Hybrid,
AllowedScopes = new List<string>
{
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.Email,
Constants.StandardScopes.OfflineAccess,
"roles",
"company",
"utc_offset",
"service_api
"
},
RequireConsent = false,
RedirectUris = new List<string>
{
REMOVED
},
PostLogoutRedirectUris = new List<string>
{
REMOVED
},
AllowedCorsOrigins = new List<string>
{
REMOVED
},
AccessTokenLifetime = 60,
IdentityTokenLifetime = 60,
AbsoluteRefreshTokenLifetime = 60 * 60 * 24,
SlidingRefreshTokenLifetime = 60 * 15,
},
#brockallen - The short of this is. I have an application that is MVC and WEBAPI and Anjulgarjs. I do not think a hybrid like this is wise but I inherited this application and now I have to find a way to make it work with Idnetity Server 3.
I would be grateful for any guidance. Please.
I was having the same issue as you. The problem is that you are setting a cookie for MVC, but you are not setting the expiration date, thus MVC does not know that the cookie has expired. What you need to do is set the expiration date for the AuthenticationTicket in the following way:
n.AuthenticationTicket.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn));
n.AuthenticationTicket.Properties.IssuedUtc = DateTime.Now;
//Add encrypted MVC auth cookie
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
I've also set the issued date, but that's not mandatory
I am trying to integrate IdentityServer4 with ASP.NET MVC WebAPI.I want to achieve role based authorization. I have the following projects running.
IdentityServer4 [a separate project]
WebApi
Javascript Application [using Extjs]
I have implemented the ResourceOwnerPassword flow and what I am trying to do is,
Make a post request to AccountController of the WebApi containing username and password
Inside the AccountController call the IdentityServer token endpoint for an access token and return the access token to the client (javascript app)
Make a request to the WebApi containing the access token.
For the above part I am success full, here is the code
POSTMAN call for Login
AccountController
[ActionName("Login")]
[AllowAnonymous]
[HttpPost]
public async Task<BaseModel> Login(LoginModel model)
{
model.RememberMe = false;
var status = await _security.Login(model.Email, model.Password, model.RememberMe);
if (status.Status == LoginStatus.Succeded)
{
return new BaseModel { success = true, message = "login", data = status.Data };
}
}
SecurityService
public async Task<LoginResponse> Login(string userName, string password, bool persistCookie = false)
{
// discover endpoints from metadata
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(userName, password, "api1");
if (tokenResponse.IsError)
{
return new LoginResponse { Status = LoginStatus.Failed, Message = tokenResponse.Error };
}
return new LoginResponse { Status = LoginStatus.Succeded, Data = tokenResponse.Json };
}
Security the API
I have two more actions inside the AccountController (just for testing) namely:
values() [returns success and requires no authentication]
SecureValues [returns success and requires authentication]
[HttpGet]
public BaseModel values()
{
return new BaseModel
{
success = true
};
}
[Authorize]
[HttpGet]
public BaseModel SecureValues()
{
return new BaseModel
{
success = true
};
}
calling "Values" action returns the success which is quite obvious, calling the "SecureValues" gives following
Which means that the user is not Authenticated.
My IdentityServer4 configuration is as follows:
public class Config
{
// scopes define the resources in your system
public static IEnumerable<Scope> GetScopes()
{
return new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.Profile,
new Scope
{
Name = "api1",
Description = "My API",
DisplayName = "API Access",
Type = ScopeType.Resource,
IncludeAllClaimsForUser = true,
Claims = new List<ScopeClaim>
{
new ScopeClaim(ClaimTypes.Name),
new ScopeClaim(ClaimTypes.Role)
}
},
new Scope
{
Enabled = true,
Name = "role",
DisplayName = "Role(s)",
Description = "roles of user",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim(ClaimTypes.Role,false)
}
},
StandardScopes.AllClaims
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" }
},
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" }
},
// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
//flow
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002" },
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
"role"
}
},
//for hybrid flow
new Client
{
ClientId = "mvchybrid",
ClientName ="mvc hybrid client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = {"http://localhost:5003/signin-oidc"},
PostLogoutRedirectUris = {"http://localhost:5003"},
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
"api1"
}
},
new Client
{
ClientId = "js",
ClientName = "javascript client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser= true,
RedirectUris = {"http://localhost:5004/callback.html"},
PostLogoutRedirectUris = {"http://localhost:5004/index.html"},
AllowedCorsOrigins = {"http://localhost:5004"},
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
"api1",
"role",
StandardScopes.AllClaims.Name
}
},
//aspnet identity client
new Client
{
ClientId = "mvcIdentity",
ClientName = "Mvc Identity Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = {"http://localhost:5005/signin-oidc"},
PostLogoutRedirectUris = {"http://localhost:5005"},
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
"api1"
}
}
};
}
public static List<InMemoryUser> GetUsers()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Subject = "1",
Username = "alice#yahoo.com",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Alice"),
new Claim("website", "https://alice.com"),
new Claim(ClaimTypes.Role,"FreeUser")
}
},
new InMemoryUser
{
Subject = "2",
Username = "bob#yahoo.com",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Bob"),
new Claim("website", "https://bob.com"),
new Claim(ClaimTypes.Role,"PaidUser")
}
}
};
}
}
WebApi configuration
public void ConfigureAuth(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(new IdentityServer3.AccessTokenValidation.IdentityServerBearerTokenAuthenticationOptions
{
Authority = "localhost:5000",
RequiredScopes = new[] { "api1" },
ClientId = "ro.client",
ClientSecret = "secret",
PreserveAccessToken = true
});
}
It looks like your Authority in your UseIdentityServerBearerTokenAuthentication middleware is wrong. I think it should be "http://localhost:5000".
Also enabling logging (with trace) to console you can see why your authorization was challenged sometimes.
I'm trying to use the AccessToken provided by Owin in Google.Apis requests but I'm receiveing the exception System.InvalidOperationException (Additional information: The access token has expired but we can't refresh it).
My configuration of Google Authentication is OK and I can successfully login into my application with it. I store the context.AccessToken as a Claim in the authentication callback (OnAuthenticated "event" of GoogleOAuth2AuthenticationProvider).
My Startup.Auth.cs configuration (app.UseGoogleAuthentication(ConfigureGooglePlus()))
private GoogleOAuth2AuthenticationOptions ConfigureGooglePlus()
{
var goolePlusOptions = new GoogleOAuth2AuthenticationOptions()
{
ClientId = "Xxxxxxx.apps.googleusercontent.com",
ClientSecret = "YYYYYYzzzzzz",
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = context =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("Google_AccessToken", context.AccessToken));
return Task.FromResult(0);
}
},
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie
};
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/plus.login");
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
return goolePlusOptions;
}
The code in which the exception is throwed (Execute() method)
var externalIdentity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
var accessTokenClaim = externalIdentity.FindAll(loginProvider + "_AccessToken").First();
var secrets = new ClientSecrets()
{
ClientId = "Xxxxxxx.apps.googleusercontent.com",
ClientSecret = "YYYYYYzzzzzz"
};
IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = secrets,
Scopes = new[] { PlusService.Scope.PlusLogin, PlusService.Scope.UserinfoEmail }
});
UserCredential credential = new UserCredential(flow, "me", new TokenResponse() { AccessToken = accessTokenClaim.Value });
var ps = new PlusService(
new BaseClientService.Initializer()
{
ApplicationName = "My App Name",
HttpClientInitializer = credential
});
var k = ps.People.List("me", PeopleResource.ListRequest.CollectionEnum.Visible).Execute();
Is there another way to get the original AccessToken or refresh it without pass thru the entire authentication process (the user is already authenticated)?
I need to query some GooglePlus profile data such as GivenName, familyName, gender, profile picture and profile url.
Linda helped me with an URL pointing to a new asp.net mvc sample (https://codereview.appspot.com/194980043/).
I just had to add AccessType = "offline" to GoogleOAuth2AuthenticationOptions and save some extra info to create a new instance of TokenResponse when I need.
Google Authentication Options
private GoogleOAuth2AuthenticationOptions ConfigureGooglePlus()
{
var goolePlusOptions = new GoogleOAuth2AuthenticationOptions()
{
AccessType = "offline",
ClientId = "Xxxxxxx.apps.googleusercontent.com",
ClientSecret = "Yyyyyyyyyy",
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = context =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("Google_AccessToken", context.AccessToken));
if (context.RefreshToken != null)
{
context.Identity.AddClaim(new Claim("GoogleRefreshToken", context.RefreshToken));
}
context.Identity.AddClaim(new Claim("GoogleUserId", context.Id));
context.Identity.AddClaim(new Claim("GoogleTokenIssuedAt", DateTime.Now.ToBinary().ToString()));
var expiresInSec = (long)(context.ExpiresIn.Value.TotalSeconds);
context.Identity.AddClaim(new Claim("GoogleTokenExpiresIn", expiresInSec.ToString()));
return Task.FromResult(0);
}
},
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie
};
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/plus.login");
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/plus.me");
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
return goolePlusOptions;
}
How to retrieve the credentials (using token info "stored" as claim)
private async Task<UserCredential> GetCredentialForApiAsync()
{
var initializer = new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "Xxxxxxxxx.apps.googleusercontent.com",
ClientSecret = "YYyyyyyyyyy",
},
Scopes = new[] {
"https://www.googleapis.com/auth/plus.login",
"https://www.googleapis.com/auth/plus.me",
"https://www.googleapis.com/auth/userinfo.email" }
};
var flow = new GoogleAuthorizationCodeFlow(initializer);
var identity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ApplicationCookie);
var userId = identity.FindFirstValue("GoogleUserId");
var token = new TokenResponse()
{
AccessToken = identity.FindFirstValue("Google_AccessToken"),
RefreshToken = identity.FindFirstValue("GoogleRefreshToken"),
Issued = DateTime.FromBinary(long.Parse(identity.FindFirstValue("GoogleTokenIssuedAt"))),
ExpiresInSeconds = long.Parse(identity.FindFirstValue("GoogleTokenExpiresIn")),
};
return new UserCredential(flow, userId, token);
}