Why am I seeing duplicate Scopes on IdentityServer4's consent screen? - identityserver4

I am writing an IdentityServer4 implementation and using the Quickstart project described here.
When you define an ApiResource (using InMemory classes for now) it looks like IdentityServer creates a Scope with the same name as the resource. For example
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api", "My API")
};
}
will create a Scope called "api" (this is done in the ApiResource constructor). If I add "api" as an allowed Scope on my Client object (using InMemoryClients for a proof of concept) and request this api Scope in the scope query string parameter in my auth request from my JavaScript client I get an invalid_scope error message.
I found by following this documentation you can add Scopes to the ApiResource through the Scopes property like so
new ApiResource
{
Name = "api",
DisplayName = "Custom API",
Scopes = new List<Scope>
{
new Scope("api.read"),
new Scope("api.write")
}
}
So now if I instead define my ApiResource like this and request the Scopes api.read and api.write (and add them to the AllowedScopes property on the Client Object) then everything works fine EXCEPT the consent page which shows duplicate Scopes. It shows api.read 2 times and api.write 2 times. See the consent screen here
The Client configuration is as follows:
new Client
{
ClientId = "client.implicit",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = { "http://localhost:3000/health-check" },
PostLogoutRedirectUris = { "http://localhost:3000" },
AllowedCorsOrigins = { "http://localhost:3000" },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"customApi.read", "customApi.write"
}
}
Why is this happening? Am I doing something obviously wrong?
Update:
Here a portion of the discovery document that shows the Scopes are only listed once...

It looks like the problem is with the Quickstart UI... or with the Scope.cs class depending on how you look at it. Specifically, in the method and line shown in the class ConsentService.cs
The following code
vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
is not filtering out the duplicates. That is, even if two Scopes have the same name they are not considered equal. So if GetHashCode and Equals were overridden in Scope.cs (which is in IdentityServer4 - not the Quickstart) then it would solve this problem. In that case SelectMany would return a unique set. This is because the ApiResources property is implemented as a HashSet. Alternatively, you could write your own logic to make this return a unique set of Scopes. This is how I solved the problem. I wrote something very similar to Jon Skeet's answer in this post that filtered out the duplicate Scopes.

The problem lies within IdentityService4 code in the implementation of InMemoryResourcesStore.FindApiResourcesByScopeAsync and was fixed with this commit. You can use the dev branch where it's included since June 22th 2017, but it was never released in any of the NuGET packages targeting .NET Standard 1.4, which is very annoying.
I created an issue and requested it to get patched:
https://github.com/IdentityServer/IdentityServer4/issues/1470
For fixing the view, i added the line marked with Todo to ConsentService.cs
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);
if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any()))
{
// TODO: Hotfix to cleanup scope duplication:
resources.ApiResources = resources.ApiResources.DistinctBy(p => p.Name).ToList();
return CreateConsentViewModel(model, returnUrl, request, client, resources);
}
This solves the display problem, but the scope will still be included multiple times in the access token which makes it bigger since it squares the scope count for that API. I had 3 scopes, so each one was included 3 times, adding 6 unneeded scope copies. But at least it's usable until it get's fixed.

There was a bug that was just fixed in 1.5 that addresses this: https://github.com/IdentityServer/IdentityServer4/pull/1030. Please upgrade and see if that fixes the issue for you. Thanks.

Related

Issue creating SamlResponse when following your example Idp code - within the LoginResponse method

I have created an IDP using the code contained within https://github.com/ITfoxtec/ITfoxtec.Identity.Saml2/blob/master/test/TestIdPCore/Controllers/AuthController.cs
This is throwing an error when I attempt to bind the authNResponse using the following code:
var responsebinding = new Saml2PostBinding();
responsebinding.Bind(saml2AuthnResponse).XmlDocument.OuterXml;
This is the same code as within the PostContent method, but I've opted to use this code direct as I just needed the SamlResponse.
The error is:
Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenWriteException: 'IDX13129: The SAML2:AttributeStatement must contain at least one SAML2:Attribute.'
With the following abridged stack trace:
at Microsoft.IdentityModel.Tokens.Saml2.Saml2Serializer.WriteAttributeStatement(XmlWriter writer, Saml2AttributeStatement statement)
at Microsoft.IdentityModel.Tokens.Saml2.Saml2Serializer.WriteStatement(XmlWriter writer, Saml2Statement statement)
at Microsoft.IdentityModel.Tokens.Saml2.Saml2Serializer.WriteAssertion(XmlWriter writer, Saml2Assertion assertion)
at Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.WriteToken(XmlWriter writer, SecurityToken securityToken)
at ITfoxtec.Identity.Saml2.Tokens.Saml2ResponseSecurityTokenHandler.WriteToken(SecurityToken token)
at ITfoxtec.Identity.Saml2.Saml2AuthnResponse.ToXml()
I have used your example code almost exactly, so is there an issue within it, or am I missing something?
Many thanks
Here's a deeper analysis and two possible solutions/workarounds.
The situation: creating a ITfoxtec.Identity.Saml2.Saml2AuthnResponse for a ClaimsIdentity that has only one claim: the nameidentifier.
The relevant code snippet (not complete, just the part that is relevant, but the ITFoxttec samples have the full code)
var response = new Saml2AuthnResponse(config);
response.ClaimsIdentity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "someone#somewhere.com") });
response.NameId = new Saml2NameIdentifier(....etc...);
var token = response.CreateSecurityToken(appliesToAddress);
//so far all is well, but the problem has been sneakily introduced!
//which is why the next line will give the error: Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenWriteException: 'IDX13129: The SAML2:AttributeStatement must contain at least one SAML2:Attribute
return binding.Bind(response).ToActionResult();
Explanation:
ITfoxtec code: The nameidentifier claim is is removed from the claims when the token is created. This makes senses as it is supposed to be in the NameId property. This remaining claims are set as Subject in the SecurityTokenDescriptor that is fed to the Saml2SecurityTokenHandler which is Microsoft code.
var tokenDescriptor = new SecurityTokenDescriptor();
tokenDescriptor.Subject = new ClaimsIdentity(claims.Where(c => c.Type != ClaimTypes.NameIdentifier));
The claims in this tokendescriptor then end up as Attributes in the AttributeStatement in the generated Saml2SecurityToken (via a Saml2SecurityTokenHandler.CreateToken(tokendescriptor) call).
Unfortunately, if the nameidentifier was the only claim you had, then you end up with an AttributeStatement that has no Attributes. And subsequently run into the problem when the binding.Bind(response) deep down the bowels does its XML thing..
Unless you are supposed to always have an AttributeStatement it looks to me like a bug / edge case in the Microsoft.IdentityModel.Tokens.Saml library.
There are two solutions to solve it:
Prevent ending up with no claims: Simply add another claim to the identity, doesn't have to be email, can be anything:
response.ClaimsIdentity.AddClaim(new Claim("x", "y"))
After the CreateSecurityToken call but before the call to Bind, check if the AttributeStatement is empty and if so remove it. A quick and dirty example for that:
var x = (Saml2AttributeStatement)token.Assertion.Statements.FirstOrDefault(a => a.GetType() == typeof(Saml2AttributeStatement));
if (x?.Attributes.Count == 0)
{
token.Assertion.Statements.Remove(x);
}
Personally, I prefer option 1, as it is generally safer to use and less code. Plus I'm sure there can always be 'something' to further attribute the identity with...
Maybe you are missing the part of adding claims to the token and creating the token?
saml2AuthnResponse.SessionIndex = sessionIndex;
var claimsIdentity = new ClaimsIdentity(claims);
saml2AuthnResponse.NameId = new Saml2NameIdentifier(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).Select(c => c.Value).Single(), NameIdentifierFormats.Persistent);
saml2AuthnResponse.ClaimsIdentity = claimsIdentity;
var token = saml2AuthnResponse.CreateSecurityToken(relyingParty.Issuer, subjectConfirmationLifetime: 5, issuedTokenLifetime: 60);
https://github.com/ITfoxtec/ITfoxtec.Identity.Saml2/blob/master/test/TestIdPCore/Controllers/AuthController.cs#L110
I have found that you need both the ClaimTypes.NameIdentifier and ClaimTypes.Email claims in order for the token to be generated successfully.

Does changed logic IdentityServer4 Scope to ApiResource and IdeneityResource

I am new at identityserver4 but used identityserver3 before. There are some new configurations named IdentityResource and ApiResource. But sample applications dos not include Scope any more. What is the difference of Scope, IdentityResource and ApiResource
The main reason for changing all that - better object model, according to the terminology. What is a scope - a resource a client wants to access. But also the scopes were 2 types: identity related and API's, and they were kind of overlapping. So here they decide to switch things a bit and separate them (into resources).
According to one of the authors of Identity server (article here) :
Resources are something you want to protect with IdentityServer – either identity data of your users (like user id, name, email..), or APIs.
Identity Resources
Standard scopes used to be defined like this:
public static IEnumerable<Scope> GetScopes()
{
return new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.Profile
};
}
..and now:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
And defining a custom one:
var customerProfile = new IdentityResource(
name: "profile.customer",
displayName: "Customer profile",
claimTypes: new[] { "name", "status", "location" });
API Resources
Before:
public static IEnumerable<Scope> GetScopes()
{
return new List<Scope>
{
new Scope
{
Name = "api1",
DisplayName = "My API #1",
Type = ScopeType.Resource
}
};
}
and now:
public static IEnumerable<ApiResource> GetApis()
{
return new[]
{
new ApiResource("api1", "My API #1")
};
}
In the article is explained a bit more, but the main idea is - use the Identity Resources when there is an Identity (Claims principal) involved, and the API resource - when an API requests that certain client must have a certain API resource to access this API.
It's a bit confusing in the beginning, but after you try it yourself, it makes a lot of sense. Good luck

Include roles in User.login() and User.getCurrent() Angular SDK

I'm currently writing an administrative interface using the Loopback Angular SDK. After having dug through the documentation and code, I'm still no wiser as to how to include the user's roles in the response. It's causing me real headaches on the frontend because I'm not yet experienced enough with Angular to figure out how to enforce a role check on each of my states (I'm using UI-Router).
client: /auth.js
// Log the user in
$scope.doAuth = function() {
$scope.hasError = false;
$scope.busy = true;
$scope.loginResult = User.login({include: 'roles'}, $scope.credentials,
function wasSuccessfulAuth(authResponse) {
$scope.busy = true;
$rootScope.isAuthenticated = true;
$rootScope.user = authResponse.user;
$location.path('dashboard');
},
function wasFailedAuth(authResponse) {
$timeout(function() {
$scope.hasError = true;
$scope.authError = authResponse.data.error.message || 'Unknown error';
$scope.busy = false;
}, 1000);
}
)
}
server: /common/models/user.json
{
"name": "user",
"plural": "Users",
"base": "User",
"properties": {
},
"relations": {
"roles": {
"type": "belongsTo",
"model": "RoleMapping",
"foreignKey": "principalId"
}
},
"acls": [],
"methods": []
}
So this works in the API explorer, I have the routes I'd expect with an object that has a relation, but I can't seem to get any further than that... All that gets returned is the standard user login stuff (id, accessToken, email, etc) The docs seem to run cold when I get this far but I'd have thought this would have been a common use case?
This is a bit of a showstopper for me.
It's actually surprisingly easy to solve this problem using LoopBack: this is where "model scopes" come in very handy -- including the default scope which I find extremely useful for this type of situation.
First, a brief explanation of model scopes:
A model scope is like a saved query or "view", that allows you to specify a built-in filter for any query for that scope. For example, if you set the default scope to a valid filter, every single query (of any kind) against your model will have this filter applied!
This can get you in a heap of trouble, but there's one use-case that's pretty safe (all other things equal) and actually addresses your question perfectly: when I said a scope lets you give a valid filter, it turns out filters aren't just where clauses, but also include, limit, etc.
So to solve your problem, you simply need a default scope on your User model that includes whatever you need to include. For your example:
Simply add a scope object to your common/models/user.json:
{
"name": "user",
"plural": "Users",
"base": "User",
"scope": {
"include": [
"roles"
]
},
"properties": {
},
"relations": {
"roles": {
"type": "belongsTo",
"model": "RoleMapping",
"foreignKey": "principalId"
}
},
"acls": [],
"methods": []
}
By adding a default scope with an "include", LoopBack will automatically embed the object(s) of the related model based on the named relation (just like if you added it in your query -- which as discussed, is not possible, or at least not easy, in this case).
One caveat: since Role and RoleMapping are built-in models and are surely marked as non-public, I am not actually sure whether you can include them directly (but for a different reason than above). I haven't worked enough with ACLs (yet), but presumably there's more complexity around access controls especially in related models.
So, whereas my example code above explain the mechanics of doing the include here, the policy concern may slow you down (I'd be curious to know if they do).
That said, however, I guess you could add a derived model (from RoleMapping) that you make public (just like you did for user), and use it instead everywhere (including in the relation to/from user) -- hopefully that's clear (let me know if not).
In conclusion: If you add a default scope to your derived user model, to do the include for you, the AngularJS service wrapper (built by lb-ng) will be none the wiser (the include all happens on the backend):
$scope.loginResult = User.login($scope.credentials,
function wasSuccessfulAuth(authResponse) {
console.log('Related models are here: ', authResponse.roles,
authResponse.user);
...
In other words, the resulting model will contain an array, .roles[], containing the related roles to this user (based on your relation definition), and .user with the entire user model right there!
This latter point is unclear but I'm confident in that, because I did exactly the above but with a different related model that I know works. And to my surprise, user was included as well, since that's explicitly requested by the LoopBack $resource wrapper (the one created by lb-ng). So, in fact, you don't have do a separate query to get the user -- it's already there! There's no API that I see when using the $service wrapper, to get at that built-in include and change it; I guess that's what was posted in a comment above.
Hope this is helpful.
Steve
User.login returns AccessToken instance. To get user role, you should make separate request to fetch user, including role.

breezejs: dynamically switching between OData and WebAPI

I'm facing a bit of a tricky situation here. I have two services that my application needs to access to. One is a pure ODATA service and the other is a WebAPI (with Breeze controller) service.
My application is designed around the AngularJS modules and breeze is injected into two differerent single-instance services :
angular.module('domiciliations.services').
factory('domiciliationService', function (breeze) {
breeze.config.initializeAdapterInstance("modelLibrary", "backingStore", true);
//more initialization goes here
//then query methods go here
}
and
angular.module('person.services').
factory('personService', function (breeze) {
breeze.config.initializeAdapterInstances({ "dataService": "OData" });
//more initialization goes here
//then query methods go here
}
Now obviously the problem is that once the person service has been instanciated, the domiciliations service then uses OData because the config was overwritten.
So, what is the general approach for tackling this issue ? Is there a way to isolate the config ?
So far the only way I can think of, is to call the initializeAdapterinstances method each time a query method is called, which is not really desirable.
EDIT
As per Jay's recommandation I'm now using DataService. I'm having an error though in ctor.resolve at the line:
ds.jsonResultsAdapter = ds.jsonResultsAdapter || ds.adapterInstance.jsonResultsAdapter;
ds.adapterInstance is null, therefore this throws an exception. But I don't understand why it's null.
Here's what I've done:
var service = new breeze.DataService({
serviceName: 'http://localhost:16361/api/mandates',
adapterName: 'WebApi'
});
var manager = new breeze.EntityManager({ dataService: service });
//this is the line causing the later exception:
manager.fetchMetadata().then(function () { ... }
Did I forget to do something ?
Good question!
The initializeAdapterInstance method is really intended to setup the 'default' adapters.
If you need to have multiple adapters and apply them on a per query basis then see the DataService documentation especially the 'adapterName' property in the ctor. You can have two DataServices, one for OData and one for WebApi. You can then use either for any query via the EntityQuery.using method.
var odataDataService = new DataService({
serviceName: "Foo",
adapterName: "OData"
});
var webApiDataService = new DataService({
serviceName: "Bar",
adapterName: "WebApi"
});
var query1 = EntityQuery.from(...).where(...);
var query2 = EntityQuery.from(...).where(...);
entityManager.executeQuery(query1.using(odataDataService)).then(...)
entityManager.executeQuery(query2.using(webApiDataService)).then(...)

How to add a filter in in the "middle of the URL" using Restlet?

I have the following routes:
/projects/{projectName}
and
/projects/{projectName}/Wall/{wallName}
Now I'd like to have that all GETs be allowed but PUT, POST, DELETE should only be allowed by project members i.e. users members of that project. I have a special class that given a user id and project name I can get the status of the user's membership - something like MyEnroler.getRole(userId, projectName) - where the userId is part of the request header and the projectName is taken from the URI.
I've tried a number of things but doesn't work. Here's the idea:
public class RoleMethodAuthorizer extends Authorizer {
#Override
protected boolean authorize(Request req, Response resp) {
//If it's a get request then no need for further authorization.
if(req.getMethod().equals(Method.GET))
return true;
else
{
String authorEmail = req.getClientInfo().getUser().getIdentifier();
String projectName = req.getAttributes().get("project").toString();
Role userRole = MyEnroler.getRole(authorEmail, projectName);
//forbid updates to resources if done by non-members of project
if(userRole.equals(MyEnroler.NON_MEMBER))
return false;
//for everybody else, return true
return true;
}
}
}
Now simply doing the following completely fails when creating inbound root in the Application:
Router projectRouter = new Router(getContext());
RoleMethodAuthorizer rma = new RoleMethodAuthorizer();
//Guard declaration here. Then setNext Restlet
guard.setNext(projectRouter);
projectRouter.attach("/projects/{project}",rma);
Router wallRouter = new Router(getContext());
wallRouter.attach("/Wall/{wallName}", WallResource.class);
rma.setNext(wallRouter);
//return guard;
So a request to /projects/stackoverflow/Wall/restlet fails. The URL is never found. I'm guessing since it's trying to match it with the projectRouter. Well I tried the various modes (MODE_BEST_MATCH or MODE_FIRST/NEXT_MATCH) to no avail.
Nothing seems to work. Conceptually this should work. I'm only intercepting a call and just being transparent to the request, but don't know how things are working on the inside.
I could move the authorizer just after the guard, but I'd lose access to the request attribute of projectName - I don't wish to parse the URL myself to search for the projectName since the URL pattern could change and would break the functionality - i.e. require 2 changes instead of one.
Any ideas how to achieve this?
I would use the standard RoleAuthorizer class to supply the list of allowed roles, along with your custom enroller probably split into two I would then add a custom Filter class that does something like this to call your Enrolers.
protected int beforeHandle(final Request request, final Response response) throws ResourceException {
final String projectName = (String) request.getAttributes().get("projectName");
// Check that a projectName is supplied, should not have got this far otherwise but lets check.
if (projectName == null || projectName.isEmpty()) {
throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND);
}
if (Method.GET.equals(request.getMethod())){
new ReadEnroler(projectName).enrole(request.getClientInfo());
}else{
new MutateEnroler(projectName).enrole(request.getClientInfo());
}
return super.beforeHandle(request, response);
}
the enrolers would then set the appropriate values in the clientInfo.getRoles() Collection when enrole was called.

Resources