Um exemplo de STS, usando IdentityServer e Identity, persistindo com RavenDB

Em um post anterior, indiquei que um servidor de identidades seria uma bela alternativa para um “primeiro microsserviço”.

Neste post, compartilho uma implementação básica, usando IdentityServer4 e Identity, usando RavenDB como mecanismo de persistência. O código-fonte está disponível no meu github.

Pela amplitude do tema, não me aprofundo em nenhuma das etapas. Mas, acho que compartilho um bom guia de como fazer as coisas acontecerem. Em casos de dúvida, compartilhe sua questão nos comentários (lembre-se, o código está no github).

Começando pela aplicação

Como de costume, resolvi separar a implementação do microsserviço de STS em uma “aplicação” e o servidor Web (que funciona como uma espécie de adapter para essa aplicação).

Quanto mais utilizo essa abordagem, mais sou simpático a ela. Pela estrutura da aplicação fica claro quais são as operações que resolvi suportar em meu STS.

Persistindo informações do IdentityServer4 no RavenDB

IdentityServer4 possui integração oficial com Entity Framework (o que torna o trabalho de persistência em SQL Server bem fácil). Entretanto, não me sinto confortável em usar um Banco de Dados relacional para essa finalidade. Tanto mais convicto em fico quando inspeciono a implementação do suporte ao EntityFramework e vejo a quantidade de tabelas que eram criadas, mappers, modelos de persistência, …

No mesmo repositório, você encontra minha proposta de persistência para RavenDB.

Como você pode ver, não é tanto código assim.

Para mostrar como a implementação é trivial, compartilho a persistência de aplicações clientes.

public interface IRavenDBClientStore : IClientStore
{
    Task StoreAsync(Client client);
    Task HasStoredClients();
}
internal class ClientStore : IRavenDBClientStore
{
    private readonly IAsyncDocumentSession _session;

    public ClientStore(IAsyncDocumentSession session)
    {
        _session = session;
    }

    public async Task FindClientByIdAsync(string clientId) =>
        (await _session.LoadAsync(RavenClient.DocId(clientId)));

    public async Task StoreAsync(Client client)
    {
        await _session.StoreAsync(client, RavenClient.DocId(client.ClientId));
        await _session.SaveChangesAsync();
    }

    public Task HasStoredClients()
    {
        return _session.Query()
            .Customize(o => o.WaitForNonStaleResults())
            .AnyAsync();
    }

    internal class RavenClient : Client
    {
        public string Id => DocId(ClientId);

        public static string DocId(string clientId)
        {
            return $"client/{clientId}";
        }
    }
}

Simplesmente adoro essa abordagem “sem configuração” do Raven. Os próprios objetos de modelo do IdentityServer4 são facilmente persistidos.

Persistindo informações do AspNetCore Identity no RavenDB

Na minha solução, utilizo a integração entre IdentityServer4 e AspNetCore Identity para salvar informações dos usuários. Novamente, não queria utilizar SQL server para salvar informações (já disse que odeio Migrations?). Por causa disso, implementei persistência do Identity também no RavenDB.

Nada demais, novamente.

Já detalhei o processo de escrita desse componente em um post anterior (se está interessado, recomendo a leitura).

Usando Docker

Como disse em um post anterior, gosto muito da ideia de usar Docker quando estou desenvolvendo microsserviços. Aqui, a composição que usei nesse exemplo.

version: '3'

services:
  playground:
    image: playground
    build:
      context: .
      dockerfile: Playground/Dockerfile
    depends_on:
      - identity.web

  identity.web:
    image: identity.web
    build:
      context: .
      dockerfile: Identity.Web/Dockerfile
    depends_on:
      - identities.data
    
  identities.data:
    image: ravendb/ravendb

Playground é o nome do serviço que escrevi em um post anterior (onde demonstro como lidar com instabilidade de conexão), e que resolvi reutilizar aqui, adicionando segurança. Vejam que estou usando RavenDB também em um container.

Aqui, o arquivo em que defino parâmetros (composição para docker).

version: '3'

services:
  playground:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://0.0.0.0:80
      - AuthenticationAuthority=http://10.0.75.1:5000
    ports:
      - "5001:80"

  identity.web:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://0.0.0.0:80
      - PlaygroundApi=http://localhost:5001
      - RavenDbUrl=http://identities.data:8080
      - RavenDbDatabase=Identities
    ports:
      - "5000:80"

  identities.data:
    ports:
      - "8080:8080"

Um detalhe muito importante é a utilização do endereço 10.0.75.1. Trata-se do endereço fornecido pelo docker para que um serviço consiga interagir com endereços da máquina host.

Antes de rodar a aplicação, é importante abrir o manager do RavenDB (localhost:8080), configurar e criar o banco de dados “Identities” que estou usando em tempo de execução.

Configurando o STS

No IdentityServer, é importante fazer as devidas configurações para indicar quais vão ser as aplicações clientes e, principalmente, que recursos essas aplicações poderão acessar (essa informação é persista no RavenDB).

Abaixo, a configuração que criei para esse exemplo:

public static IEnumerable GetApis() => new[]
{
    new ApiResource("playground", "PlaygroundService")
};

public static IEnumerable GetResources() => new IdentityResource[]
{
    new IdentityResources.OpenId(),
    new IdentityResources.Profile()
};

public static IEnumerable GetClients(
    IDictionary<string, string> clientsUrl
) => new[]
{
    new Client
    {
        ClientId = "playgroundswaggerui",
        ClientName = "Playground Swagger UI",
        AllowedGrantTypes = GrantTypes.Implicit,
        AllowAccessTokensViaBrowser = true,

        RedirectUris = { $"{clientsUrl["PlaygroundApi"]}/swagger/o2c.html" },
        PostLogoutRedirectUris = { $"{clientsUrl["PlaygroundApi"]}/swagger/" },

        AllowedScopes =
        {
            "playground"
        },
        RequireConsent = false
    } 
};

Explicar detalhes desse código está além dos meus objetivos nesse post. Entretanto, é fácil reconhecer que estamos criando configuração para protejer uma API e estamos nos preparando para suportar uma aplicação cliente (no caso, o swagger da api que desenvolvemos no exemplo do post anteerior).

Abaixo, a inicialização da aplicação de identidade.

using System;
using BuildingBlocks.AspnetCoreIdentity.RavenDB;
using Identity.Application.Setup;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Identity.Application
{
    public static class Startup
    {
        public static IServiceCollection AddIdentityApplication(
            this IServiceCollection services, IConfiguration configuration
        )
        {
            services.AddDocumentStore(configuration,
                new CollectionMapping(typeof(IdentityResource), "IdentityResources")
                );

            services.AddRavenDBIdentity(
                DocumentStoreHolder.Store,
                DocumentStoreHolder.DatabaseName
            );

            services.Configure(options =>
            {
                // Password settings
                options.Password.RequireDigit = false;
                options.Password.RequiredLength = 6;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
                options.Password.RequireLowercase = false;
                options.Password.RequiredUniqueChars = 1;
            });

            services.ConfigureApplicationCookie(options =>
            {
                // Cookie settings
                options.Cookie.HttpOnly = true;
                options.Cookie.Expiration = TimeSpan.FromDays(150);
                options.LoginPath = "/Accounts/Login";
                options.LogoutPath = "/Accounts/Logout";
                options.AccessDeniedPath = "/Accounts/AccessDenied";
                options.SlidingExpiration = true;
            });

            services
                .AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddRavenDBConfigurationStore(
                    DocumentStoreHolder.Store,
                    DocumentStoreHolder.DatabaseName,
                    async (clientStore, resourceStore) => await BaseConfig.LoadSeed(
                        clientStore, resourceStore, configuration
                        )
                )
                .AddRavenDBOperationalStore(
                    DocumentStoreHolder.Store,
                    DocumentStoreHolder.DatabaseName
                )
                .AddAspNetIdentity()
                .AddProfileService();

            return services;
        }

        public static IApplicationBuilder UseIdentityApplication(
            this IApplicationBuilder app
        )
        {
            app.UseRavenDBIdentity();
            app.UseAuthentication();
            app.UseIdentityServer();
            return app;
        }
    }
}

Configurando a API a ser protegida

A configuração da API que iremos proteger é simples.

using System.IdentityModel.Tokens.Jwt;
using BuildingBlocks.Core;
using BuildingBlocks.Mvc;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
    public static class SetupIdentity
    {
        public static IServiceCollection AddCustomIdentity(
            this IServiceCollection services, 
            IApiInfo apiInfo
            )
        {
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

            }).AddJwtBearer(options =>
            {
                options.Authority = apiInfo.AuthenticationAuthority;
                options.RequireHttpsMetadata = false;
                options.Audience = apiInfo.JwtBearerAudience;
            });

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddScoped<IUser, HttpContextUser>();

            return services;
        }
    }
}

Basicamente, configuro qual será a origem do token que será utilizado na autenticação.

Configurando o Swagger

Para o cliente Swagger funcionar adequadamente, precisamos informar detalhes do servidor de identidades.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

using Swashbuckle.AspNetCore.Examples;
using Swashbuckle.AspNetCore.Swagger;

using BuildingBlocks.Core;

namespace BuildingBlocks.Swagger
{
    public static class Startup
    {
        public static IServiceCollection AddCustomSwagger(
            this IServiceCollection services,
            IApiInfo apiInfo

        ) => services
            .AddSwaggerGen(options =>
            {
                options.DescribeAllEnumsAsStrings();

                options.SwaggerDoc(apiInfo.Version, new Info
                {
                    Title = apiInfo.Title,
                    Version = apiInfo.Version,
                    Description = apiInfo.Version
                });

                if (apiInfo.AuthenticationAuthority != null)
                {
                    options.AddSecurityDefinition("oauth2", new OAuth2Scheme
                    {
                        Type = "oauth2",
                        Flow = "implicit",
                        AuthorizationUrl = $"{apiInfo.AuthenticationAuthority}/connect/authorize",
                        TokenUrl = $"{apiInfo.AuthenticationAuthority}/connect/token",
                        Scopes = apiInfo.Scopes
                    });
                }
                
                options.OperationFilter(apiInfo);
                options.OperationFilter();
                options.OperationFilter();
            });

        public static IApplicationBuilder UseCustomSwagger(
            this IApplicationBuilder app,
            IApiInfo apiInfo
        ) => app
            .UseSwagger()
            .UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint($"/swagger/{apiInfo.Version}/swagger.json", $"{apiInfo.Title} {apiInfo.Version}");
                if (apiInfo.AuthenticationAuthority != null)
                {
                    c.ConfigureOAuth2(
                        apiInfo.SwaggerAuthInfo.ClientId,
                        apiInfo.SwaggerAuthInfo.Secret,
                        apiInfo.SwaggerAuthInfo.Realm,
                        $"{apiInfo.Title} - ${apiInfo.Version} - Swagger UI"
                    );
                }
            });
    }
}

Resultado

Ao executar a aplicação acessando o serviço que está protegido, tenho acesso, na interface do próprio swagger, ao mecanismo que irá me encaminhar para o servidor de autenticação.

Concluindo

No código de hoje, passamos por AspNetCore Identity, IdentityServer4, RavenDB e Docker.

Um bocado de tecnologias que você deveria conhecer (acho que já conhece).  Como resultado, temos um exemplo consistente de como construir um microsserviço de STS.

Caso tenha dúvidas ou sugestões, compartilhe nos comentários.

Crédito da capa para unsplash-logoHans-Peter Gauster

Compartilhe este insight:

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mais insights para o seu negócio

Veja mais alguns estudos e reflexões que podem gerar alguns insights para o seu negócio:

Nesse ano, palestrei na APIX sobre microsserviços. Abaixo, registro em vídeo feito pela organização do evento. Comentários? Feedback?
Gosto bastante da abordagem de Caitie McCaffrey para explicar sagas. Neste post, me inspiro na linha de raciocínio dela para...
Nem todos os problemas podem ser resolvidos da mesma forma. Nem toda ferramenta é apropriada para todo tipo de trabalho....
Neste post, gostaria de compartilhar a estrutura que venho adotando em meus projetos com microsserviços. São algumas ideias que tenho...
Empresas modernas, com estilo de gestão diferente e resultados espetaculares, estão desafiando tudo o que sabemos sobre estratégia e execução....
Are you designing Microservices? So, I would like to share a fascinating slide deck that I discovered recently. That comes...

Curso Reputação e Marketing Pessoal

Masterclasses

01

Introdução do curso

02

Por que sua “reputação” é importante?

03

Como você se apresenta?

04

Como você apresenta suas ideias?

05

Como usar Storytelling?

06

Você tem uma dor? Eu tenho o alívio!

07

Escrita efetiva para não escritores

08

Como aumentar (e manter) sua audiência?

09

Gatilhos! Gatilhos!

10

Triple Threat: Domine Produto, Embalagem e Distribuição

11

Estratégias Vencedoras: Desbloqueie o Poder da Teoria dos Jogos

12

Análise SWOT de sua marca pessoal

13

Soterrado por informações? Aprenda a fazer gestão do conhecimento pessoal, do jeito certo

14

Vendo além do óbvio com a Pentad de Burkle

15

Construindo Reputação através de Métricas: A Arte de Alinhar Expectativas com Lag e Lead Measures

16

A Tríade da Liderança: Navegando entre Líder, Liderado e Contexto no Mundo do Marketing Pessoal

17

Análise PESTEL para Marketing Pessoal

18

Canvas de Proposta de Valor para Marca Pessoal

19

Método OKR para Objetivos Pessoais

20

Análise de Competências de Gallup

21

Feedback 360 Graus para Autoavaliação

22

Modelo de Cinco Forças de Porter

23

Estratégia Blue Ocean para Diferenciação Pessoal

24

Análise de Tendências para Previsão de Mercado

25

Design Thinking para Inovação Pessoal

26

Metodologia Agile para Desenvolvimento Pessoal

27

Análise de Redes Sociais para Ampliar Conexões

Lições complementares

28

Apresentando-se do Jeito Certo

29

O mercado remunera raridade? Como evidenciar a sua?

30

O que pode estar te impedindo de ter sucesso

Recomendações de Leituras

31

Aprendendo a qualificar sua reputação do jeito certo

32

Quem é você?

33

Qual a sua “IDEIA”?

34

StoryTelling

35

Você tem uma dor? Eu tenho o alívio!

36

Escrita efetiva para não escritores

37

Gatilhos!

38

Triple Threat: Domine Produto, Embalagem e Distribuição

39

Estratégias Vencedoras: Desbloqueie o Poder da Teoria do Jogos

40

Análise SWOT de sua marca pessoal

Inscrição realizada com sucesso!

No dia da masterclass você receberá um e-mail com um link para acompanhar a aula ao vivo. Até lá!

A sua subscrição foi enviada com sucesso!

Aguarde, em breve entraremos em contato com você para lhe fornecer mais informações sobre como participar da mentoria.

Crie sua conta

Preencha os dados para iniciar o seu cadastro no plano anual do Clube de Estudos:

Crie sua conta

Preencha os dados para iniciar o seu cadastro no plano mensal do Clube de Estudos:

× Precisa de ajuda?