I've been scratching my head on this problem for quite a few days already. I have a problem with IdentityServer4 where the token response does not contain a refresh token. The code I have already works fine regarding getting the access token - it's just the refresh token that I cannot seem to figure out.
In my project.json file, I have added the following entries:
"IdentityServer4": "1.0.0-rc3",
"IdentityServer4.AccessTokenValidation": "1.0.1-rc3"
My Startup.cs file contains the following:
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddInMemoryClients(Config.Clients.Get())
.AddInMemoryScopes(Config.Scopes.Get())
.AddInMemoryUsers(Config.Users.Get())
.AddTemporarySigningCredential();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseIdentityServer();
}
You may have noticed that I am using Config.Clients, Config.Scope, and Config.Users. Here is the code for those (Config.cs):
public class Config
{
internal class Clients
{
public static IEnumerable<Client> Get()
{
return new List<Client> {
new Client
{
ClientId = "client",
ClientName = "Authentication Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
AccessTokenLifetime = 1800,
RefreshTokenExpiration = TokenExpiration.Absolute,
AbsoluteRefreshTokenLifetime = 1800,
ClientSecrets = new List<Secret> {
new Secret("secret".Sha256())
},
AllowedScopes = new List<string>
{
"api",
StandardScopes.OfflineAccess.Name, //For refresh tokens
}
}
};
}
}
internal class Scopes
{
public static IEnumerable<Scope> Get()
{
return new List<Scope>
{
StandardScopes.OfflineAccess, //For refresh tokens
new Scope {
Name = "api",
DisplayName = "API",
Description = "API scope",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim> {
new ScopeClaim(JwtClaimTypes.Role)
},
ScopeSecrets = new List<Secret> {
new Secret("secret".Sha256())
}
}
};
}
}
internal class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser>
{
new InMemoryUser {
Subject = "5BE86359-073C-434B-AD2D-A3932222DABE",
Username = "admin",
Password = "admin",
Claims = new List<Claim> {
new Claim(JwtClaimTypes.Role, "admin")
}
}
};
}
}
}
And I am calling this IdentityController.cs in order to get the Token:
[Route("api/[controller]")]
public class IdentityController : ControllerBase
{
[HttpPost("GetToken")]
public string GetToken()
{
DiscoveryResponse dr = new DiscoveryClient("http://localhost:5000").GetAsync().Result;
TokenClient tokenClient = new TokenClient(dr.TokenEndpoint, "client", "secret", AuthenticationStyle.BasicAuthentication);
TokenResponse tokenResponse = tokenClient.RequestClientCredentialsAsync("api").Result;
return tokenResponse.AccessToken; //"tokenResponse" does not contain the refresh token!?
}
}
I have read quite a few articles, but no where could I find a place where they focus on the Refresh Token alone. They all seem to focus just on basic code that returns the Access Token.
Can someone tell me what I am missing, and maybe show/point me in the right direction? Any help will be greatly appreciated!
You need to include the offline_access scope when requesting the token.
But the client credentials flow does not have support for refresh tokens - so this will not work.
Related
Summary
I have ASP.NET MVC 5 web app with Identity authentication and I have to develop an API with "grant_type" = "authorization_code". This API will be to provide users data to another "well-known" web service that needs a custom error responses. My IDE is Visual Studio Professional 2017. I use Postman to make requests to my Web API.
Documentation I read
In the OWIN and Katana documentation the OWIN OAuth 2.0 Authorization Server link redirects again to main OWIN and Katana page, but I think that I found the source on GitHub: OWIN OAuth 2.0 Authorization Server. I tried to follow this documentation, but there are no examples about this question.
Problem
I can create a new authorization code in my AuthorizationCodeProvider class (with Create() method) when a user authenticates and authorizes the "well-known" web service client to access user's resources. I store this code in a database. When I request a Token AuthorizationCodeProvider.Receive() method is called and the token is deserialized correctly. Then GrantAuthorizationCode() method is called, Postman receives OK response (200 status code) but without token information in body (.AspNet.ApplicationCookie is in cookies).
Detailed explanation and code
This is the Startup class:
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),
OnApplyRedirect = (context =>
{
// This code is to return custom error response
string path = null;
if (context.Request.Path.HasValue)
path = context.Request.Path.Value;
if (!(path != null && path.Contains("/api"))) // Don't redirect to login page
context.Response.Redirect(context.RedirectUri);
})
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
this.ConfigureAuthorization(app);
}
private void ConfigureAuthorization(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
OAuthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = false,
TokenEndpointPath = new PathString("/api/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new TokenAuthorizationServerProvider(),
AuthorizationCodeProvider = new AuthorizationCodeProvider()
};
app.Use<AuthenticationMiddleware>(); //Customize responses in Token middleware
app.UseOAuthAuthorizationServer(OAuthOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
ConfigureAuthorization() method configures the authorization. It uses classes implemented by me:
AuthenticationMiddleware: the well-known web service wants 401 status responses with custom error JONS instead of the usual 400 status response. It is based on the answer of the question Replace response body using owin middleware.
public class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
var owinResponse = context.Response;
var owinResponseStream = owinResponse.Body;
var responseBuffer = new MemoryStream();
owinResponse.Body = responseBuffer;
await Next.Invoke(context);
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest &&
context.Response.Headers.ContainsKey(BearerConstants.CustomUnauthorizedHeaderKey))
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
string headerValue = context.Response.Headers.Get(BearerConstants.CustomUnauthorizedHeaderKey);
context.Response.Headers.Remove(BearerConstants.CustomUnauthorizedHeaderKey);
ErrorMessage errorMessage = new ErrorMessage(headerValue);
string json = JsonConvert.SerializeObject(errorMessage, Formatting.Indented);
var customResponseBody = new StringContent(json);
var customResponseStream = await customResponseBody.ReadAsStreamAsync();
await customResponseStream.CopyToAsync(owinResponseStream);
owinResponse.ContentType = "application/json";
owinResponse.ContentLength = customResponseStream.Length;
owinResponse.Body = owinResponseStream;
}
}
}
When ErrorMessage is serialized to JSON returns an array of errors:
{
"errors":
[
"message": "the error message"
]
}
I set the BearerConstants.CustomUnauthorizedHeaderKey header in TokenAuthorizationServerProvider.ValidateClientAuthentication() method using a extension method:
public static void Rejected(this OAuthValidateClientAuthenticationContext context, string message)
{
Debug.WriteLine($"\t\t{message}");
context.SetError(message);
context.Response.Headers.Add(BearerConstants.CustomUnauthorizedHeaderKey, new string[] { message });
context.Rejected();
}
This is how TokenAuthorizationServerProvider is implemented:
public class TokenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
// Only for breakpoint. Never stops.
return base.AuthorizeEndpoint(context);
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Check if grant_type is authorization_code
string grantType = context.Parameters[BearerConstants.GrantTypeKey];
if (string.IsNullOrEmpty(grantType) || grantType != BearerConstants.GrantTypeAuthorizationCode)
{
context.Rejected("Invalid grant type"); // Sets header for custom response
return;
}
// Check if client_id and client_secret are in the request
string clientId = context.Parameters[BearerConstants.ClientIdKey];
string clientSecret = context.Parameters[BearerConstants.ClientSecretKey];
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
{
context.Rejected("Client credentials missing"); // Sets header for custom response
return;
}
//Check if client_id and client_secret are valid
ApiClient client = await (new ApiClientService()).ValidateClient(clientId, clientSecret);
if (client != null)
{
// Client has been verified.
Debug.WriteLine($"\t\tClient has been verified");
context.OwinContext.Set<ApiClient>("oauth:client", client);
context.Validated(clientId);
}
else
{
// Client could not be validated.
context.Rejected("Invalid client"); // Sets header for custom response
}
}
public override async Task GrantAuthorizationCode(OAuthGrantAuthorizationCodeContext context)
{
TokenRequestParameters parameters = await context.Request.GetBodyParameters();
using (IUserService userService = new UserService())
{
ApplicationUser user = await userService.ValidateUser(parameters.Code);
if (user == null)
{
context.Rejected("Invalid code");
return;
}
// Initialization.
var claims = new List<Claim>();
// Setting
claims.Add(new Claim(ClaimTypes.Name, user.UserName));
// Setting Claim Identities for OAUTH 2 protocol.
ClaimsIdentity oAuthClaimIdentity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesClaimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);
// Setting user authentication.
IDictionary<string, string> data = new Dictionary<string, string>{ { "userName", user.UserName } };
AuthenticationProperties properties = new AuthenticationProperties(data);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthClaimIdentity, properties);
// Grant access to authorize user.
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesClaimIdentity);
}
}
}
ApiClientService.ValidateClient() checks on database that cliend ID and Secret are correct.
GrantAuthorizationCode() is based on the step 8 from ASP.NET MVC - OAuth 2.0 REST Web API Authorization Using Database First Approach tutorial. But this tutorial for grant_type = password and I think that something is wrong in here.
And the AuthorizationCodeProvider class:
public class AuthorizationCodeProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
AuthenticationTicket ticket = context.Ticket;
string serializedTicket = context.SerializeTicket();
context.SetToken(serializedTicket);
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
// At this point context.Ticket.Identity.IsAuthenticated is true
}
}
I call to create method from the AuthorizationController that shows the Allow/Deny view. It is decorated with System.Web.Mvc.Authorize attribute, so if the user isn't authenticated he or she has to login using the default login page from MVC template project (/account/login):
[Authorize]
public class AuthorizationController : Controller
{
private const string ServiceScope = "service-name";
[HttpGet]
public async Task<ActionResult> Index(string client_id, string response_type, string redirect_uri, string scope, string state)
{
AuthorizationViewModel vm = new AuthorizationViewModel()
{
ClientId = client_id,
RedirectUri = redirect_uri,
Scope = scope,
State = state
};
if (scope == ServiceScope)
{
var authentication = HttpContext.GetOwinContext().Authentication;
authentication.SignIn(
new AuthenticationProperties { IsPersistent = true, RedirectUri = redirect_uri },
new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, User.Identity.Name) },
"Bearer"));
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
[MultiButton(MatchFormKey = "authorization", MatchFormValue = "Allow")]
public async Task<ActionResult> Allow(AuthorizationViewModel vm)
{
if (ModelState.IsValid)
{
string code = await this.SetAuthorizationCode(vm.ClientId, vm.RedirectUri);
if (vm.Scope == ServiceScope)
{
string url = $"{vm.RedirectUri}?code={code}&state={vm.State}";
return Redirect(url);
}
else
{
return Redirect(vm.RedirectUri);
}
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
[MultiButton(MatchFormKey = "authorization", MatchFormValue = "Deny")]
public async Task<ActionResult> Deny(AuthorizationViewModel vm)
{
// Removed for brevity
return View(vm);
}
private async Task<string> SetAuthorizationCode(string clientId, string redirectUri)
{
string userId = User.Identity.GetUserId();
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(clientId, OAuthDefaults.AuthenticationType));
AuthenticationTokenCreateContext authorizeCodeContext = new AuthenticationTokenCreateContext(
HttpContext.GetOwinContext(),
Startup.OAuthOptions.AuthorizationCodeFormat,
new AuthenticationTicket(
identity,
new AuthenticationProperties(new Dictionary<string, string>
{
{ "user_id", userId },
{ "client_id", clientId },
{ "redirect_uri", redirectUri }
})
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.Add(Startup.OAuthOptions.AuthorizationCodeExpireTimeSpan)
}));
Startup.OAuthOptions.AuthorizationCodeProvider.Create(authorizeCodeContext);
string code = authorizeCodeContext.Token;
IUserService userService = new UserService();
await userService.SetAuthorization(userId, true, code); // save to database
userService.Dispose();
return code;
}
}
The authorization code is created in SetAuthorizationCode() method, which is called in Allow() action. This SetAuthorizationCode() method code is based on this answer.
Questions
I now that is very long with a lot of code, but I'm stuck for some days and I didn't find the solution. I don't know the complete flow of the authorization, I think that I'm missing something.
What happens when I call /api/token? I mean, what are the steps in this part of the authentication/authorization flow?
What happens after AuthorizationCodeProvider.GrantAuthorizationCode()?
Why a cookie returned instead of token in the body?
I found the solution of the problem, it was the AuthenticationMiddleware. Once the body of the response is read, it remains empty and does not reach the client. So you have to rewrite the response body.
public class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
var owinResponse = context.Response;
var owinResponseStream = owinResponse.Body;
var responseBuffer = new MemoryStream();
owinResponse.Body = responseBuffer;
await Next.Invoke(context);
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest &&
context.Response.Headers.ContainsKey(BearerConstants.CustomUnauthorizedHeaderKey))
{
// Customize the response
}
else
{
// Set body again with the same content
string body = Encoding.UTF8.GetString(responseBuffer.ToArray());
StringContent customResponseBody = new StringContent(body);
Stream customResponseStream = await customResponseBody.ReadAsStreamAsync();
await customResponseStream.CopyToAsync(owinResponseStream);
}
}
}
I'm trying to setup and IdentiyServer4, but I am stuck at the authorize:calling the /connect/introspect endpoint gives me the error
IdentityServer4.Validation.ApiSecretValidator: Error: No API resource with that name found. aborting
IdentityServer4.Endpoints.IntrospectionEndpoint: Error: API unauthorized to call introspection endpoint. aborting.
The client is a net framework 4.7 MVc and uses the IdentityServer3.AccesTokenValidation package
Here's my Identity Server configuration
internal class Resources
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "role",
UserClaims = new List<string> {"role"}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new[]
{
new ApiResource
{
Name = "electronicinvoice",
DisplayName = "electronicinvoice",
Description = "electronicinvoice",
Scopes = new List<string> { "electronicinvoice" },
ApiSecrets = new List<Secret> {new Secret("XXXXX".Sha256())},
UserClaims = new List<string> {"role"}
}
};
}
public static IEnumerable<ApiScope> GetApiScopes()
{
return new[]
{
new ApiScope("electronicinvoice", "Access to electronicinvoiceactive api"),
};
}
}
The Client:
internal class Clients
{
public static IEnumerable<Client> Get()
{
ICollection<string> allowed = GrantTypes.ClientCredentials.Union(GrantTypes.ResourceOwnerPassword).ToList();
return new List<Client>
{
new Client
{
ClientId = "SolutionUpdate",
ClientName = "Legal SolutionDOC client",
AllowedGrantTypes =allowed ,
ClientSecrets = new List<Secret> {new Secret("XXXXX".Sha256())},
AllowedScopes = new List<string> {"email","openid","profile","electronicinvoice" },
}
};
}
}
the Startup method
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddInMemoryClients(Clients.Get())
.AddInMemoryIdentityResources(Resources.GetIdentityResources())
.AddInMemoryApiResources(Resources.GetApiResources())
.AddInMemoryApiScopes(Resources.GetApiScopes())
.AddDeveloperSigningCredential()
.AddProfileService<ProfileService>()
.AddCustomTokenRequestValidator<TokenRequestValidator>();
services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
services.AddTransient<IProfileService, ProfileService>();
}
And, ofc, the client configuration
public void Configuration(IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions()
{
Authority = "https://localhost:44389",
ClientId = "SolutionUpdate",
ClientSecret = "XXXXXX",
ValidationMode = ValidationMode.ValidationEndpoint
});
}
Now, i can succesfully get a valid token using this method
var client = new TokenClient("https://localhost:44389/connect/token", "SolutionUpdate", "XXXXX");
var extra = new Dictionary<string, string> { { nameof(paramAuth.CustomerCode), paramAuth.ToJson() } };
var response = client.RequestClientCredentialsAsync("electronicinvoice" , extra).Result;
var token = response.AccessToken;
return Content(new DTO.GetTokenResponse { Token = token }.ToJson(), "application/json");
but i can't access any method decorated with the Authorize attribute.
I Also tried calling directly the introspection endpoint like this
var introspectionClient = new IntrospectionClient("https://localhost:44389/connect/introspect", "SolutionUpdate", "XXXXXX");
var response = introspectionClient.SendAsync(new IntrospectionRequest { Token = accessToken }).Result;
var isActive = response.IsActive;
var claims = response.Claims;
or from postman,
POST /connect/introspect
Authorization: basic (with username and password) and body
Token = myaccesstoken
Any suggestion is welcome
Nb: i doublecked the passwords i'm using and they are all correct
Ok, i figured it out:
The introspection endpoint wants a basic authentication with the Apiscope credentials.
This behaviour was probabilly different in identityserver3, or i am missing a configuration somewhere.
So I have 2 workarounds:
-Change the Apiscope name and password to be the same as the clientId and password
-Implement my own AuthorizeAttribute, in wich i'll call the introspection endpoint and parse the the response.
I'll probabilly go with the second, it feels less "hackish", and i fear the first workaround will give me problem when i'll setup the token encryption
i want to get custom claims via the userinfo endpoint. That works too but all of the custom claims are in access token too. I understand it in that way, that there is a possibility where i can put one or two claims (like email or name) in the access token and all of the other claims (given_name,...) are accessed via the userinfo endpoint. (and they are not in the access token)
my ProfileService is looking like that:
public class ProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<CustomUser> _claimsFactory;
private readonly UserManager<CustomUser> _userManager;
public ProfileService(UserManager<CustomUser> userManager, IUserClaimsPrincipalFactory<CustomUser> claimsFactory)
{
_claimsFactory = claimsFactory;
_userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
claims.Add(new Claim(JwtClaimTypes.GivenName, user.FirstName));
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
context.IsActive = user != null;
}
}
this is my config.cs file:
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API", new [] {JwtClaimTypes.Name })
};
}
new Client
{
ClientId = "xxx",
ClientName = "xxx",
//AccessTokenType = AccessTokenType.Reference,
AccessTokenType = AccessTokenType.Jwt,
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
RedirectUris = { "http://localhost:4200/home" },
PostLogoutRedirectUris = { "http://localhost:4200/unauthorized" },
AllowedCorsOrigins = { "http://localhost:4200" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"api1"
}
}
};
Did i missunderstand it?
GetProfileDataAsync is called twice, but with a different context.
For the access token Context.Caller = ClaimsProviderAccessToken.
For the identity token Context.Caller = UserInfoEndpoint.
Please note that for both contexts the requested claims differ.
If you want to add claims for the identity only, you can either configure identityserver to include these claims by adding these to the filter (IdentityResource), in which case you won't need to add additional claims in GetProfileDataAsync at all. Or in case you want to add a specific claim, check the current context.
So in GetProfileDataAsync you could have something like the following:
if (Context.Caller == "UserInfoEndpoint")
claims.Add(new Claim(JwtClaimTypes.GivenName, user.FirstName));
This should add the claim to the userinfo only.
Ok, so this one has been highly frustrating and I cannot seem to figure out how to fix it, hoping someone can point me in the right direction.
So if I may give a run down of the architecture:
Solution
IdentityServer Layer
WebApi Layer
MVC layer (as the client)
The Issue
When trying to access the user that has been authenticated by the IdentityServer it always returns false
if (this.User.Identity.IsAuthenticated)
{
var identity = this.User.Identity as ClaimsIdentity;
foreach (var claim in identity.Claims)
{
Debug.Write(claim.Type + " " + claim.Value);
}
}
I've spent many hours trying to figure why I do not have access to the user.
IdentityServer
public void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
var idServerServiceFactory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get())
.UseInMemoryUsers(Users.Get());
var options = new IdentityServerOptions
{
Factory = idServerServiceFactory,
SiteName = "Proderty Security Token Service",
IssuerUri = "https://localhost:44300/identity",
PublicOrigin = "https://localhost:44300/",
SigningCertificate = LoadCertificate()
};
idsrvApp.UseIdentityServer(options);
});
}
public static IEnumerable<Client> Get()
{
return new[]
{
new Client()
{
ClientId = "prodertyclientcredentials",
ClientName = "Proderty (Client Credentials)",
Flow = Flows.ClientCredentials,
ClientSecrets = new List<Secret>()
{
new Secret("Some Secret here".Sha256())
},
AllowAccessToAllScopes = true
},
new Client()
{
ClientId = "prodertyauthcode",
ClientName = "Proderty (Authorization Code)",
Flow = Flows.AuthorizationCode,
RedirectUris = new List<string>()
{
"http://localhost:10765/prodertycallback"
},
ClientSecrets = new List<Secret>()
{
new Secret("Some Secret here".Sha256())
},
AllowAccessToAllScopes = true
},
new Client()
{
ClientId = "prodertyhybrid",
ClientName = "Proderty (Hybrid)",
Flow = Flows.Hybrid,
RedirectUris = new List<string>()
{
"http://localhost:10765"
},
AllowAccessToAllScopes = true
}
};
}
public static IEnumerable<Scope> Get()
{
return new List<Scope>()
{
new Scope
{
Name = "prodertymanagment",
DisplayName = "Proderty Management",
Description = "Allow the application to manage proderty on your behalf.",
Type = ScopeType.Resource
},
StandardScopes.OpenId,
StandardScopes.Profile
};
}
return new List<InMemoryUser>()
{
new InMemoryUser()
{
Username = "Terry",
Password = "Bob",
Subject = "b053d546-6ca8-b95c-77e94d705ddf",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Terry"),
new Claim(Constants.ClaimTypes.FamilyName, "Wingfield")
}
},
new InMemoryUser()
{
Username = "John",
Password = "Bob",
Subject = "bb61e881-3a49-8b62-c13dbe102018",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "John"),
new Claim(Constants.ClaimTypes.FamilyName, "Borris")
}
}
};
As you can see the IdentityServer seems positively intact. Once called I expect that I should be access the user on the MVC client.
WebApi
public void Configuration(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44300/identity",
RequiredScopes = new[] { "prodertymanagment" }
});
}
[Authorize]
public class TestController : ApiController
{
// GET api/<controller>
public string GetOne()
{
return "If you can see this, then we must be authorized! - GETONE - Coming From Test Controller";
}}
}
MVCClient
public static HttpClient GetClient()
{
var client = new HttpClient();
var accessToken = Token.RequestAccessTokenAuthorizationCode();
if (accessToken != null)
{
client.SetBearerToken(accessToken);
}
client.BaseAddress = new Uri("https://localhost:44301/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}
public static string RequestAccessTokenAuthorizationCode()
{
var cookie = HttpContext.Current.Request.Cookies.Get("ProdertyCookie");
if (cookie != null && cookie["access_token"] != null)
{
return cookie["access_token"];
}
var authorizeRequest = new IdentityModel.Client.AuthorizeRequest(
"https://localhost:44300/identity/connect/authorize"
);
var state = HttpContext.Current.Request.Url.OriginalString;
var url = authorizeRequest.CreateAuthorizeUrl(
"prodertyauthcode",
"code",
"prodertymanagment",
"http://localhost:10765/prodertycallback",
state);
HttpContext.Current.Response.Redirect(url);
return null;
}
// GET: ProdertyCallback
public async Task<ActionResult> Index()
{
var authCode = Request.QueryString["code"];
var client = new TokenClient(
"https://localhost:44300/identity/connect/token",
"prodertyauthcode",
"Some Secret here"
);
var tokenResponse = await client.RequestAuthorizationCodeAsync(
authCode,
"http://localhost:10765/prodertycallback"
);
Response.Cookies["ProdertyCookie"]["access_token"] = tokenResponse.AccessToken;
return Redirect(Request.QueryString["state"]);
}
With the code above everything works, If not logged in I'm pushed to the IdentityServer login page, My callback fires and then returned to the originating page. I also have a access token.
Now that I'm authorized to access the page, I expect to have access to the user but I don't which is highly frustrating.
I'm very aware that it's the app that's being authenticated but I'm sure the purpose was to access the user that authenticated the app (As a user of the client system too).
public async Task<ActionResult> Index()
{
if (this.User.Identity.IsAuthenticated)
{
var identity = this.User.Identity as ClaimsIdentity;
foreach (var claim in identity.Claims)
{
Debug.Write(claim.Type + " " + claim.Value);
}
}
var httpClient = ProdertyHttpClient.GetClient();
var test = await httpClient.GetAsync("api/test/getone").ConfigureAwait(false);
if (test.IsSuccessStatusCode)
{
var stringContent = await test.Content.ReadAsStringAsync().ConfigureAwait(false);
return Content(stringContent);
}
else
{
return Content(ExceptionHelper.GetExceptionFromResponse(test).ToString());
}
}
The only thing that springs to mind is using the Authorize Attribute which attempts to use the core asp.net identity model which I have not install because I'll have a custom data store.
Any help greatly appreciated!
I apologize in advance for asking this as I have next to no knowledge of security in general and IdentityServer in particular.
I am trying to set up IdentityServer to manage security for an Asp.Net MVC application.
I am following the tutorial on their website: Asp.Net MVC with IdentityServer
However, I am doing something slightly different in that I have a separate project for the Identity "Server" part, which leads to 2 Startup.cs files, one for the application and one for the Identity Server
For the application, the Startup.cs file looks like this
public class Startup
{
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
RedirectUri = "https://localhost:44300/",
ResponseType = "id_token token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
userInfo.Claims.ToList().ForEach(c => nid.AddClaim(new Claim(c.Item1, c.Item2)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
// keep track of access token expiration
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
}
});
app.UseResourceAuthorization(new AuthorizationManager());
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
RequiredScopes = new[] { "baseballStatsApi"}
});
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
For the identity server, the startup.cs file is
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = InMemoryFactory.Create(
users: Users.Get(),
clients: Clients.Get(),
scopes: Scopes.Get())
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(#"{0}\bin\Configuration\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
I am also setting up an Authorization Manager
public class AuthorizationManager : ResourceAuthorizationManager
{
public override Task<bool> CheckAccessAsync(ResourceAuthorizationContext context)
{
switch (context.Resource.First().Value)
{
case "Players":
return CheckAuthorization(context);
case "About":
return CheckAuthorization(context);
default:
return Nok();
}
}
private Task<bool> CheckAuthorization(ResourceAuthorizationContext context)
{
switch(context.Action.First().Value)
{
case "Read":
return Eval(context.Principal.HasClaim("role", "LevelOneSubscriber"));
default:
return Nok();
}
}
}
So for instance, if I define a controller method that is decorated with the ResourceAuthorize attribute, like so
public class HomeController : Controller
{
[ResourceAuthorize("Read", "About")]
public ActionResult About()
{
return View((User as ClaimsPrincipal).Claims);
}
}
Then, when I first try to access this method, I will be redirected to the default login page.
What I don't understand however, is why when I login with the user I have defined for the application (see below),
public class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Username = "bob",
Password = "secret",
Subject = "1",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Bob"),
new Claim(Constants.ClaimTypes.FamilyName, "Smith"),
new Claim(Constants.ClaimTypes.Role, "Geek"),
new Claim(Constants.ClaimTypes.Role, "LevelOneSubscriber")
}
}
};
}
}
I get a 403 error, Bearer error="insufficient_scope".
Can anybody explain what I am doing wrong?
Any subsequent attempt to access the action method will return the same error. It seems to me that the user I defined has the correct claims to be able to access this method. However, the claims check only happens once, when I first try to access this method. After I login I get a cookie, and the claims check is not made during subsequent attempts to access the method.
I'm a bit lost, and would appreciate some help in clearing this up.
Thanks in advance.
EDIT: here are the scoles and client classes
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
Name = "baseballStatsApi",
Description = "Access to baseball stats API",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
}
};
scopes.AddRange(StandardScopes.All);
return scopes;
}
}
And the Client class
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "Baseball Stats Emporium",
ClientId = "baseballStats",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://localhost:44300/"
}
},
new Client
{
Enabled = true,
ClientName = "Baseball Stats API Client",
ClientId = "baseballStats_Api",
ClientSecrets = new List<ClientSecret>
{
new ClientSecret("secret".Sha256())
},
Flow = Flows.ClientCredentials
}
};
}
}
I have also created a custom filter attribute which I use to determine when the claims check is made.
public class CustomFilterAttribute : ResourceAuthorizeAttribute
{
public CustomFilterAttribute(string action, params string[] resources) : base(action, resources)
{
}
protected override bool CheckAccess(HttpContextBase httpContext, string action, params string[] resources)
{
return base.CheckAccess(httpContext, action, resources);
}
}
The breakpoint is hit only on the initial request to the url. On subsequent requests, the filter attribute breakpoint is not hit, and thus no check occurs. This is surprising to me as I assumed the check would have to be made everytime the url is requested.
You need to request the scopes required by the api when the user logs in.
Scope = "openid profile roles baseballStatsApi"
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
ResponseType = "id_token token",
RedirectUri = "https://localhost:44300/",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,