Skip to content
This repository has been archived by the owner on Jul 31, 2024. It is now read-only.

Self-registering services #1248

Closed
1 task done
pjc3320 opened this issue Jun 12, 2017 · 13 comments
Closed
1 task done

Self-registering services #1248

pjc3320 opened this issue Jun 12, 2017 · 13 comments
Labels

Comments

@pjc3320
Copy link

pjc3320 commented Jun 12, 2017

  • 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?

@TomCJones
Copy link

TomCJones commented Jun 12, 2017 via email

@leastprivilege
Copy link
Member

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/

@agilenut
Copy link

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.

@TomCJones
Copy link

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.

@leastprivilege
Copy link
Member

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.

@nicbavetta
Copy link

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

@TomCJones
Copy link

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.

@TomCJones
Copy link

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.

@agilenut
Copy link

@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.

@nicbavetta
Copy link

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

@agilenut
Copy link

@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.

@vanillajonathan
Copy link

vanillajonathan commented Oct 16, 2018

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; }
    }
}

@lock
Copy link

lock bot commented Jan 12, 2020

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.

@lock lock bot locked as resolved and limited conversation to collaborators Jan 12, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

6 participants