Identityserver4: Self-registering services

Created on 12 Jun 2017  Â·  13Comments  Â·  Source: IdentityServer/IdentityServer4

  • [x] I read and understood how to enable logging

Are there any patterns or best practices for registering new ApiResources and their scopes with IdentityServer? In our environment, we have multiple APIs (Resource Servers) which we would like to keep decoupled from our Identity Server implementation. Each of those APIs would need to somehow register themselves with Identity Server. This could possibly happen on deployment or on startup. We've seen the API Client Registration specification here. We will also need to do something like this as well for our internal API Clients. What would you recommend as a secure way of registering these? Are you planning on releasing any new endpoints to handle either of these?

question

Most helpful comment

I was in need of client registration so I wrote some code after peeking at the OpenID Connect Dynamic Client Registration 1.0 and RFC 7591 (OAuth 2.0 Dynamic Registration) specifications.

The code is not compliant to the specifications.

  • It does not return any error object with the error codes.
  • It does not have any GET method.

    • Hence returns 200 OK instead of 201 Created.

I use the IdentityServer4.EntityFramework package hence gets ConfigurationDbContext dependency injected into the controller. If you do not use the IdentityServer4.EntityFramework package, you can dependency inject your own DbContext.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Example.IdentityServer.Models;

namespace Example.IdentityServer.Controllers
{
    [Route("connect/register")]
    [ApiController]
    // [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    [Consumes("application/json")]
    [Produces("application/json")]
    public class ClientController : ControllerBase
    {
        private readonly ConfigurationDbContext _context;

        public ClientController(ConfigurationDbContext context)
        {
            _context = context;
        }

        // POST: connect/register
        [HttpPost]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public async Task<IActionResult> PostAsync(ClientRegistrationModel model)
        {
            if (!Request.IsHttps)
            {
                return BadRequest("HTTPS is required at this endpoint.");
            }

            if (model.GrantTypes == null)
            {
                model.GrantTypes = new List<string> { OidcConstants.GrantTypes.AuthorizationCode };
            }

            if (model.GrantTypes.Any(x => x == OidcConstants.GrantTypes.Implicit) || model.GrantTypes.Any(x => x == OidcConstants.GrantTypes.AuthorizationCode))
            {
                if (!model.RedirectUris.Any())
                {
                    return BadRequest("A redirect URI is required for the supplied grant type.");
                }

                if (model.RedirectUris.Any(redirectUri => !Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)))
                {
                    return BadRequest("One or more of the redirect URIs are invalid.");
                }
            }

            var response = new ClientRegistrationResponse
            {
                ClientId = Guid.NewGuid().ToString(),
                ClientSecret = GenerateSecret(32),
                ClientName = model.ClientName,
                ClientUri = model.ClientUri,
                LogoUri = model.LogoUri,
                GrantTypes = model.GrantTypes,
                RedirectUris = model.RedirectUris,
                Scope = model.Scope
            };

            var client = new Client
            {
                ClientId = response.ClientId,
                ClientName = response.ClientName,
                ClientSecrets = new List<ClientSecret>(),
                ClientUri = model.ClientUri,
                LogoUri = model.LogoUri,
                AllowedGrantTypes = new List<ClientGrantType>(),
                AllowedScopes = new List<ClientScope>(),
                RedirectUris = new List<ClientRedirectUri>()
            };

            client.ClientSecrets.Add(new ClientSecret
            {
                Value = response.ClientSecret, 
                Client = client
            });

            foreach (var scope in model.Scope.Split())
            {
                client.AllowedScopes.Add(new ClientScope
                {
                    Scope = scope,
                    Client = client
                });
            }

            foreach (var grantType in model.GrantTypes)
            {
                client.AllowedGrantTypes.Add(new ClientGrantType { Client = client, GrantType = grantType });
            }

            foreach (var redirectUri in model.RedirectUris)
            {
                client.RedirectUris.Add(new ClientRedirectUri { Client = client, RedirectUri = redirectUri });
            }

            _context.Clients.Add(client);

            await _context.SaveChangesAsync();

            return Ok(response);
        }
}
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using IdentityModel;
using Newtonsoft.Json;

namespace Example.IdentityServer.Models
{
    public class ClientRegistrationModel
    {
        [JsonProperty(OidcConstants.ClientMetadata.ClientName)]
        public string ClientName { get; set; }

        [JsonProperty(OidcConstants.ClientMetadata.ClientUri)]
        [Url]
        public string ClientUri { get; set; }

        [JsonProperty(OidcConstants.ClientMetadata.LogoUri)]
        [Url]
        public string LogoUri { get; set; }

        [JsonProperty(OidcConstants.ClientMetadata.GrantTypes)]
        public IEnumerable<string> GrantTypes { get; set; }

        [JsonProperty(OidcConstants.ClientMetadata.RedirectUris)]
        public IEnumerable<string> RedirectUris { get; set; } = new List<string>();

        public string Scope { get; set; } = "openid profile email";
    }
}
using IdentityModel;
using Newtonsoft.Json;

namespace Example.IdentityServer.Models
{
    public class ClientRegistrationResponse : ClientRegistrationModel
    {
        [JsonProperty(OidcConstants.RegistrationResponse.ClientId)]
        public string ClientId { get; set; }

        [JsonProperty(OidcConstants.RegistrationResponse.ClientSecret)]
        public string ClientSecret { get; set; }
    }
}

All 13 comments

It seems to be a general problem with all open ID specs. I would like to see some formal method for establishing a trust framework for all back channel servers. Any one up for that?

..Tom's phone

On Jun 12, 2017, at 1:51 PM, Paul Crossan <[email protected]notifications@github.com> wrote:

Are there any patterns or best practices for registering new ApiResources and their scopes with IdentityServer? In our environment, we have multiple APIs (Resource Servers) which we would like to keep decoupled from our Identity Server implementation. Each of those APIs would need to somehow register themselves with Identity Server. This could possibly happen on deployment or on startup. We've seen the API Client Registration specification herehttp://openid.net/specs/openid-connect-registration-1_0.html. We will also need to do something like this as well for our internal API Clients. What would you recommend as a secure way of registering these? Are you planning on releasing any new endpoints to handle either of these?

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHubhttps://github.com/IdentityServer/IdentityServer4/issues/1248, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AKxq1itUvcfCittgzpYwQq8VbiNf4FjTks5sDaTGgaJpZM4N3qtu.

We discussed this a couple of times. Registration is nothing we gonna build into the core of IdentityServer. But you could easily add the "write" endpoints yourself.

Since we think this is more of a task for an admin UI/API - see here:
https://www.identityserver.com/products/

Yes, we could add the write end points ourselves but, as @TomCJones mentioned, the difficulty is setting up some kind of dynamic back channel trust mechanism that is sufficiently secure and, at the same time, isn't overly burdensome for the services that are self registering.

I'd be very curious what others are doing with regard to this. @TomCJones if you have thoughts I'd love to hear them.

Here are some quick initial thoughts:

  • For back channel registration, a separate registration API could be created with the write endpoints. Im thinking this should be a separate API because it would likely need to be secured differently than identity due to its back channel nature (public vs private) and it would also have very different scaling needs since it would receive the largest load as services within the environment came online.
  • Both identity and the registration API could use the same underlying database (perhaps via a shared library). Normally, I'd want each microservice to have its own DB but this might be warranted to allow for the separation.
  • I like the idea of the registration token used in the response of the dynamic client registration spec but that puts the burden of storage on the self registering service. Some micro services might not need their own persistence mechanism. So, I'm inclined to let the registering service provide their own client Id and secret (complexity could be validated by the registration service). That way, those items could be injected into the service's config on deployment. This would also make handling multiple service instances easier.
  • The registration API would be network secured but could also require a pre-seeded API client that it creates on startup which could be used by the registering services. The pre-seeded client could be the only client allowed to use the register scope associated with that service. Alternatively, it could require some kind of HMAC signature based a on a secret key that is injected into the starting services and the registration service on deployment.

Obviously some of these thoughts are specific to a 1st party scenario running in the same network.

I wrote up some ideas on this here: https://wiki.idesg.org/wiki/index.php?title=Trust_Framework_Membership_Validation

I would welcome any comments about it as I believe we need to create some sort of standard or best practice in this area.

I would recommend you start a separate project around that - I don't see this as a core IdentityServer concern. Keep us posted on the progress though.

@TomCJones @agilenut Have either of you progressed forward with this? I also need to register services dynamically.

Well, there are a few developments, mostly in the UK. I earlier decided that the OpenID federation docs were not helpful. The UK references are not final, and require a certificate for each "member", which I would not mandate. But you can get some ideas of the json exchanges here: https://openbanking.atlassian.net/wiki/spaces/DZ/pages/28737919/The+Open+Banking+Directory+-+v1.1.1-rc1#TheOpenBankingDirectory-v1.1.1-rc1-GenerateSoftwareStatementAssertion

I am working on 2fa now, but remain interested in some sort of membership end-point at some time.

btw, I look at this as "just" an endpoint with a Get query and a json response. I would be happy to move a real world implementation into a standards track somewhere. I would expect that anon access is fine, but would like to hear from others if that makes sense.

@nicbavetta We went forward with the approach that I outlined in my earlier comment. It's similar to the Dynamic Client Registration spec. We are doing this for both Clients and API Resources. This approach seems like it will work for us but it is still very early. APIs are just starting to be ported over to the new registration model. So, we'll see how well it works in practice.

Again, our scenario is that we wanted several different 1st party development teams to be able to quickly spin up new Clients and APIs without requiring tedious environment configurations. Keeping registration within a 1st party trust boundary makes our scenario easier than a public or true 3rd party registration scenario. We were able to leverage our continuous deployment tooling to securely store and distribute the secret material to each new API that needed to register itself with our Identity Server implementation as that API came online.

An added benefit of this style of self-registration is that the API Resource and Client configuration becomes code in the registered API that goes through our normal PR approval process.

There are several areas of improvement that we'd like to see. One area that we have discussed is the desire for a kind of scope policy which would allow a team creating an API to define the criteria that should be met to allow a client to register for an allowed scope.

@agilenut Do you have any code that can be shared for this? I am only interested in the 1st party trust boundary.

@nicbavetta Sorry. At this time, all of the code that we have is interwoven with quite a bit of proprietary logic that I cannot currently share.

I was in need of client registration so I wrote some code after peeking at the OpenID Connect Dynamic Client Registration 1.0 and RFC 7591 (OAuth 2.0 Dynamic Registration) specifications.

The code is not compliant to the specifications.

  • It does not return any error object with the error codes.
  • It does not have any GET method.

    • Hence returns 200 OK instead of 201 Created.

I use the IdentityServer4.EntityFramework package hence gets ConfigurationDbContext dependency injected into the controller. If you do not use the IdentityServer4.EntityFramework package, you can dependency inject your own DbContext.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Example.IdentityServer.Models;

namespace Example.IdentityServer.Controllers
{
    [Route("connect/register")]
    [ApiController]
    // [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    [Consumes("application/json")]
    [Produces("application/json")]
    public class ClientController : ControllerBase
    {
        private readonly ConfigurationDbContext _context;

        public ClientController(ConfigurationDbContext context)
        {
            _context = context;
        }

        // POST: connect/register
        [HttpPost]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public async Task<IActionResult> PostAsync(ClientRegistrationModel model)
        {
            if (!Request.IsHttps)
            {
                return BadRequest("HTTPS is required at this endpoint.");
            }

            if (model.GrantTypes == null)
            {
                model.GrantTypes = new List<string> { OidcConstants.GrantTypes.AuthorizationCode };
            }

            if (model.GrantTypes.Any(x => x == OidcConstants.GrantTypes.Implicit) || model.GrantTypes.Any(x => x == OidcConstants.GrantTypes.AuthorizationCode))
            {
                if (!model.RedirectUris.Any())
                {
                    return BadRequest("A redirect URI is required for the supplied grant type.");
                }

                if (model.RedirectUris.Any(redirectUri => !Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)))
                {
                    return BadRequest("One or more of the redirect URIs are invalid.");
                }
            }

            var response = new ClientRegistrationResponse
            {
                ClientId = Guid.NewGuid().ToString(),
                ClientSecret = GenerateSecret(32),
                ClientName = model.ClientName,
                ClientUri = model.ClientUri,
                LogoUri = model.LogoUri,
                GrantTypes = model.GrantTypes,
                RedirectUris = model.RedirectUris,
                Scope = model.Scope
            };

            var client = new Client
            {
                ClientId = response.ClientId,
                ClientName = response.ClientName,
                ClientSecrets = new List<ClientSecret>(),
                ClientUri = model.ClientUri,
                LogoUri = model.LogoUri,
                AllowedGrantTypes = new List<ClientGrantType>(),
                AllowedScopes = new List<ClientScope>(),
                RedirectUris = new List<ClientRedirectUri>()
            };

            client.ClientSecrets.Add(new ClientSecret
            {
                Value = response.ClientSecret, 
                Client = client
            });

            foreach (var scope in model.Scope.Split())
            {
                client.AllowedScopes.Add(new ClientScope
                {
                    Scope = scope,
                    Client = client
                });
            }

            foreach (var grantType in model.GrantTypes)
            {
                client.AllowedGrantTypes.Add(new ClientGrantType { Client = client, GrantType = grantType });
            }

            foreach (var redirectUri in model.RedirectUris)
            {
                client.RedirectUris.Add(new ClientRedirectUri { Client = client, RedirectUri = redirectUri });
            }

            _context.Clients.Add(client);

            await _context.SaveChangesAsync();

            return Ok(response);
        }
}
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using IdentityModel;
using Newtonsoft.Json;

namespace Example.IdentityServer.Models
{
    public class ClientRegistrationModel
    {
        [JsonProperty(OidcConstants.ClientMetadata.ClientName)]
        public string ClientName { get; set; }

        [JsonProperty(OidcConstants.ClientMetadata.ClientUri)]
        [Url]
        public string ClientUri { get; set; }

        [JsonProperty(OidcConstants.ClientMetadata.LogoUri)]
        [Url]
        public string LogoUri { get; set; }

        [JsonProperty(OidcConstants.ClientMetadata.GrantTypes)]
        public IEnumerable<string> GrantTypes { get; set; }

        [JsonProperty(OidcConstants.ClientMetadata.RedirectUris)]
        public IEnumerable<string> RedirectUris { get; set; } = new List<string>();

        public string Scope { get; set; } = "openid profile email";
    }
}
using IdentityModel;
using Newtonsoft.Json;

namespace Example.IdentityServer.Models
{
    public class ClientRegistrationResponse : ClientRegistrationModel
    {
        [JsonProperty(OidcConstants.RegistrationResponse.ClientId)]
        public string ClientId { get; set; }

        [JsonProperty(OidcConstants.RegistrationResponse.ClientSecret)]
        public string ClientSecret { get; set; }
    }
}

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings