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
Related
I'm attempting to call Identity Server 4 with Flurl
Im getting a 400 back from the ID server.
The credentials etc work when I make the call with PostMan or with the methods available in IdentityModel.Client.
I am unsure about the Post i.e. PostUrlEncodedAsync
As you can see I have tried various combinations.
This.. does not work
var address = "http://localhost:8027/connect/token";
var x = await address
.WithBasicAuth("clientName", "secretValue")
.SetQueryParams(
new
{
GrantType = "client_credentials",
Scope = "requiredScope"
}
)
.PostUrlEncodedAsync(new { });
//.SendAsync(HttpMethod.Post, null, CancellationToken.None);
//.SendAsync(HttpMethod.Post, y);
This.. does.
var apiClientCredentials = new ClientCredentialsTokenRequest()
{
Address = "http://localhost:8027/connect/token",
ClientId = "clientName",
ClientSecret = "secretValue",
Scope = "requiredScope"
};
var client = new HttpClient();
var tokenResponse = await client.RequestClientCredentialsTokenAsync(apiClientCredentials);
if (tokenResponse.IsError)
{
}
I can see that deep doen inside 'RequestClientCredentialsTokenAsync' it does parameters.Add to add grant type and scope hence I have added those as params.
Have also tried adding the client id and secret to the query params.
Same 400 message.
Any help greatly appreciated.
I believe the difference is that IdentityModel knows to serialize those ClientCredentialsTokenRequest property names like ClientId and GrantType to snake-case, i.e. client_id and grant_type, as required by OAuth2. When you hand Flurl an anonymous object, it just serializes the property names exactly as-is. To make it work with Flurl you can either create a class and decorate it with Json.NET serialization attributes, or (what I typically do) keep it simple take some liberties with C# property name conventions:
.SetQueryParams(
new
{
grant_type = "client_credentials",
scope = "requiredScope"
}
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.
OK, I'm stuck on what should be a basic task in ExtJs. I'm writing a simple login script that sends a user name and password combination to a RESTful web service and receives a GUID if the credentials are correct.
My question is, do I use a Model Proxy or a Store Proxy?
To my understanding, Models represent a single record, whereas Stores are for handling sets of data containing more than one record. If this is correct then it would seem that a Model proxy is the way to go.
Following Sencha's documentation at http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.data.Model the code would look something like this:
Ext.define('AuthenticationModel', {
extend: 'Ext.data.Model',
fields: ['username', 'password'],
proxy: {
type: 'rest',
url : '/authentication'
}
});
//get a reference to the authentication model class
var AuthenticationModel = Ext.ModelManager.getModel('AuthenticationModel');
So far everything is OK, until the next step:
//Use the configured RestProxy to make a GET request
AuthenticationModel.load('???', {
success: function(session) {
console.log('Login successful');
}
});
The load() method for the Model class is a static call expecting a single unique identifier. Logins typically depend upon two factors, username and password.
So it appears Store proxies are the only way to validate someone's username and password credential combination in ExtJS. Can someone verify and explain? Any help to understand this would be greatly appreciated.
You just need to know the following:
The store will use it's own proxy if you configured one for this
instance and if not he takes the proxy from the model.
So you can easily go with two proxy configurations to enable the multi-CRUD operations on the store and the single-CRUD operations on the Models. Note the the static load method of the Model expects the model id because it is supposed to load a model by just one Id (yes, composite keys are not supported). You will also have to fetch the model instance in the callback (As you did).
Back to your Username/password problem
You may apply your session Model with a custom 'loadSession' method
loadSession: function(username,password, config) {
config = Ext.apply({}, config);
config = Ext.applyIf(config, {
action: 'read',
username: username,
password: password
});
var operation = new Ext.data.Operation(config),
scope = config.scope || this,
callback;
callback = function(operation) {
var record = null,
success = operation.wasSuccessful();
if (success) {
record = operation.getRecords()[0];
// If the server didn't set the id, do it here
if (!record.hasId()) {
record.setId(username); // take care to apply the write ID here!!!
}
Ext.callback(config.success, scope, [record, operation]);
} else {
Ext.callback(config.failure, scope, [record, operation]);
}
Ext.callback(config.callback, scope, [record, operation, success]);
};
this.getProxy().read(operation, callback, this);
}
Now call this instead of load.
I found it in the documentation of sencha App Architecture Part 2
Use proxies for models:
It is generally good practice to do this as it allows you to load and
save instances of this model without needing a store. Also, when
multiple stores use this same model, you don’t have to redefine your
proxy on each one of them.
Use proxies for stores:
In Ext JS 4, multiple stores can use the same data model, even if the
stores will load their data from different sources. In our example,
the Station model will be used by the SearchResults and the Stations
store, both loading the data from a different location. One returns
search results, the other returns the user’s favorite stations. To
achieve this, one of our stores will need to override the proxy
defined on the model.
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.
From a visualforce page, I need to retrieve our organization's salesforce instance's URL, and not the visual force URL.
For example I need https://cs1.salesforce.com instead of https://c.cs1.visual.force.com
Here's what I've tried so far and the outcome I got:
Accessed the Site global variable from the VF Page:
<apex:outputText value="{!$Site.Domain}" /> returns null
Sidenote: Everything in $Site.xxx seems to return null.
From the Apex controller:
public String getSfInstance()
{
return ApexPages.currentPage().getHeaders().get('Host');
}
and
public String getSfInstance()
{
return URL.getSalesforceBaseUrl().toExternalForm();
}
returns c.cs1.visual.force.com and https://c.cs1.visual.force.com, respectively.
Question: How do I retrieve what I want: https://cs1.salesforce.com?
Here's something that I used within my Apex Trigger
System.URL.getSalesforceBaseUrl().getHost().remove('-api' );
This gives me proper URL
This is a known issue, the URL.getSalesforceBaseUrl() should provide this information but it does not. However in reality this has very limited functional impact.
Their instance and apex domains are interchangeable in the sense that requesting a URL that does not belong to one gets redirected to the other.
for example if you seek /apex/myPage from cs1.salesforce.com you'll get redirected to c.cs1... and vise versa requesting /ID from apex domain will get you redirected to instance domain (unless detail action has been overridden)
If this does not help you there is one workaround, albeit very ugly :) create a custom object to store the base url and create before insert/update trigger which will set the baseURL field to URL.getSalesforceBaseUrl().toExternalForm(). Apparently trigger is the only place on the platform where this will work (aside from execute anonymous which is not of much use). When setting up the app insert something into that table and later use SOQL to retrieve base url.
Here is an Apex property that you can throw into a Utility class that will reliably return the instance for your org. Using this, you can easily construct your organization's Salesforce URL by appending ".salesforce.com" to the Instance:
public class Utils {
// Returns the Salesforce Instance that is currently being run on,
// e.g. na12, cs5, etc.
public static String Instance {
public get {
if (Instance == null) {
//
// Possible Scenarios:
//
// (1) ion--test1--nexus.cs0.visual.force.com --- 5 parts, Instance is 2nd part
// (2) na12.salesforce.com --- 3 parts, Instance is 1st part
// (3) ion.my.salesforce.com --- 4 parts, Instance is not determinable
// Split up the hostname using the period as a delimiter
List<String> parts = System.URL.getSalesforceBaseUrl().getHost().replace('-api','').split('\\.');
if (parts.size() == 3) Instance = parts[0];
else if (parts.size() == 5) Instance = parts[1];
else Instance = null;
} return Instance;
} private set;
}
// And you can then get the Salesforce base URL like this:
public static String GetBaseUrlForInstance() {
return 'https://' + Instance + '.salesforce.com';
}
FYI: For Scenario (1), the 1st of the 4-part hostname can get really complicated, but you'll always be able to find the Instance name as the 2nd part. For those who are interested, the syntax of Scenario 1 follows this pattern:
<MyDomain>--<SandboxName>--<Namespace>.<Instance>.visual.force.com
Here you have a quite nice and small snippet, that does, what it should for VisualforcePages :-)
String sUrlRewrite = System.URL.getSalesforceBaseUrl().getHost();
// Example: c.cs7.visual.force.com
sUrlRewrite = 'https://'
+ sUrlRewrite.substring(2,6)
+ 'salesforce.com'
+ '/'
+ recordId;
// Returns: https://cs7.salesforce.com/00kM00000050jFMIAY
Use: Url.getOrgDomainUrl().toExternalForm()
Thanks, Tim Lewis
Note behaviour changes between releases and is sensitive to My Domain settings:
#Future context returns https://na1.salesforce.com
Visualforce context returns https://na1.salesforce.com
Force.com Site context returns https://na1.salesforce.com
#Future context returns https://mydomain.my.salesforce.com
Visualforce context returns https://mydomain.my.salesforce.com
Force.com Site context returns https://mydomain.my.salesforce.com
My Domain is mandatory in new orgs effective Winter '21.
Enhanced Domains is mandatory in all orgs effective Summer '22.
// Not to be confused with Url.getSalesforceBaseUrl()
// http://na1.salesforce.com (can happen in async apex)
// https://c.na1.visual.force.com (local Visualforce Page)
// https://ns.na1.visual.force.com (packaged Visualforce Page)
// https://custom.my.salesforce.com (org has My Domain enabled)
// https://sandbox-mydomain.na1.force.com (sandbox site with My Domain...)
See also the Salesforce Identity API which attests the pod/instance endpoint.
Fix to Alex_E snippet:
String sUrlRewrite = System.URL.getSalesforceBaseUrl().getHost();
String sfBaseProtocol = System.URL.getSalesforceBaseUrl().getProtocol();
//remove namespace
integer firstDotPos = sUrlRewrite.indexOf('.');
sURlRewrite = sURlRewrite.substring(firstDotPos+1);
//replace visual.force with salesforce
sURlRewrite = sURlRewrite.replace('visual.force', 'salesforce');
sUrlRewrite = sfBaseProtocol+'://'+sURlRewrite;
serverURL = sUrlRewrite;
This works for me:
String sUrlRewrite = System.URL.getSalesforceBaseUrl().getProtocol()
+ '://' + System.URL.getSalesforceBaseUrl().getHost()
+ '/' + record.Id;
Here is something to do with regex
public String getURL() {
return String.format(
'https://{0}.salesforce.com',
new String[]{
URL.getSalesforceBaseUrl().getHost().substringAfter('.').substringBefore('.')
});
}