IdentityServer / IdentityServer4

OpenID Connect and OAuth 2.0 Framework for ASP.NET Core
https://identityserver.io
Apache License 2.0
9.21k stars 4k forks source link

Self-registering services #1248

Closed pjc3320 closed 7 years ago

pjc3320 commented 7 years ago

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 commented 7 years ago

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 notifications@github.com<mailto: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.

leastprivilege commented 7 years ago

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 commented 7 years ago

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:

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

TomCJones commented 7 years ago

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 commented 7 years ago

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 commented 6 years ago

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

TomCJones commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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

agilenut commented 6 years ago

@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 commented 5 years ago

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.

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[bot] commented 4 years ago

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.