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,
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'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.
We have an Azure Mobile App using social network authentication. Trying to add user roles as claims using a custom token handler.
This all works when running on localhost -- the tokens are added in the token handler and they are available when the AuthorizationAttribute OnAuthorization method is called. The Authorize Attribute with the Roles specified works as expected.
But when running is Azure -- the claims are added but when the OnAuthorization method is called the custom role claims are gone.
Here is the code:
Startup/Config Class
public class OwinStartup
{
public void Configuration(IAppBuilder app)
{
var config = GlobalConfiguration.Configuration;
new MobileAppConfiguration()
.AddPushNotifications()
.ApplyTo(config);
MobileAppSettingsDictionary settings = config.GetMobileAppSettingsProvider().
GetMobileAppSettings();
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions()
{
SigningKey = ConfigurationManager.AppSettings["authSigningKey"],
ValidAudiences = new[] { ConfigurationManager.AppSettings["authAudience"] },
ValidIssuers = new[] { ConfigurationManager.AppSettings["authIssuer"] },
TokenHandler = new AppServiceTokenHandlerWithCustomClaims(config)
});
//Authenticate stage handler in OWIN Pipeline
app.Use((context, next) =>
{
return next.Invoke();
});
app.UseStageMarker(PipelineStage.Authenticate);
}
Token Handler that Adds the Role Claims
public class AppServiceTokenHandlerWithCustomClaims : AppServiceTokenHandler
{
public AppServiceTokenHandlerWithCustomClaims(HttpConfiguration config)
: base(config)
{
}
public override bool TryValidateLoginToken(
string token,
string signingKey,
IEnumerable<string> validAudiences,
IEnumerable<string> validIssuers,
out ClaimsPrincipal claimsPrincipal)
{
var validated = base.TryValidateLoginToken(token, signingKey, validAudiences, validIssuers, out claimsPrincipal);
if (validated)
{
string sid = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier).Value;
var roleProvider = UnityConfig.Container.Resolve<IRoleProvider>("RoleProvider");
var roles = roleProvider.GetUserRolesBySid(sid);
foreach (var role in roles)
{
((ClaimsIdentity)claimsPrincipal.Identity).AddClaim(new Claim(ClaimTypes.Role, role));
}
}
return validated;
}
}
Role Claim
An example of a role claim from the identity claims collection
{http://schemas.microsoft.com/ws/2008/06/identity/claims/role: admin}
Authorize Attribute on Web Api Controller
[Authorize(Roles = "admin")]
Every call to an endpoint that has an Authorize attribute with one or more roles specified fails (401)
Not sure what is going on with the claims either getting stripped off or not persisted in the Identity when running in Azure.
thanks
Michael
I've got a chapter on this in the book - https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter2/custom/#using-third-party-tokens
Note the bit about custom authentication with additional claims. You need to call a custom API with the original token, check the token for validity, then produce a new token (the zumo token) with the claims you want. You can then use those claims for anything that is required.
According to this blog post, some of your options may be wrong. The AppSettings are only set for local debugging, and won't work in Azure.
Try this:
public void Configuration(IAppBuilder app)
{
var config = GlobalConfiguration.Configuration;
new MobileAppConfiguration()
.AddPushNotifications()
.ApplyTo(config);
MobileAppSettingsDictionary settings = config
.GetMobileAppSettingsProvider()
.GetMobileAppSettings();
// Local Debugging
if (string.IsNullOrEmpty(settings.HostName))
{
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions()
{
SigningKey = ConfigurationManager.AppSettings["authSigningKey"],
ValidAudiences = new[] { ConfigurationManager.AppSettings["authAudience"] },
ValidIssuers = new[] { ConfigurationManager.AppSettings["authIssuer"] },
TokenHandler = new AppServiceTokenHandlerWithCustomClaims(config)
});
}
// Azure
else
{
var signingKey = GetSigningKey();
string hostName = GetHostName(settings);
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
SigningKey = signingKey,
ValidAudiences = new[] { hostName },
ValidIssuers = new[] { hostName },
TokenHandler = new AppServiceTokenHandlerWithCustomClaims(config)
});
}
//Authenticate stage handler in OWIN Pipeline
app.Use((context, next) =>
{
return next.Invoke();
});
app.UseStageMarker(PipelineStage.Authenticate);
}
private static string GetSigningKey()
{
// Check for the App Service Auth environment variable WEBSITE_AUTH_SIGNING_KEY,
// which holds the signing key on the server. If it's not there, check for a SigningKey
// app setting, which can be used for local debugging.
string key = Environment.GetEnvironmentVariable("WEBSITE_AUTH_SIGNING_KEY");
if (string.IsNullOrWhiteSpace(key))
{
key = ConfigurationManager.AppSettings["SigningKey"];
}
return key;
}
private static string GetHostName(MobileAppSettingsDictionary settings)
{
return string.Format("https://{0}/", settings.HostName);
}
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!