Azure AD Autenticação e Autorização Entendendo os Enpoints OAuth 2.0
Published Jun 13 2022 02:32 PM 1,958 Views
Microsoft

Fluxo de Autorização Azure AD.png

Documentação Microsoft sobre os endpoint Oauth2.0 https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow

Proposta

 

Estamos passando por um processo contínuo de inovação digital, os desafios nas jornadas de modernização de aplicações são enormes, seja na criação de aplicações 100% Nuvem, ou na migração de aplicações mais antigas, por isso toda ajuda que tivermos é bem-vinda. Esse é o casso de processos de gestão de identidade, estes estão cada vez mais complexos, críticos e modernos. Delegar essa responsabilidade para uma ferramenta SAAS como Azure Active Directory pode ser uma grande vantagem na sua jornada de Modernização.

O Microsoft Azure AD é uma plataforma de identidade na nuvem que fornece uma experiência completa para gestão de identidade (IDaaS). Com ele podemos proteger nossas aplicações usando protocolos como SAML 2.0, OpenID Connect, OAuth 2.0 e WS-Federation.

A plataforma é bem completa fornece logon único (SSO), autenticação multifator, acesso condicional para proteção contra os principais ataques de segurança, além de um ecossistema de serviços que permite integração com o Active Directory on-premises, é um universo enorme de possibilidades.

Mas aqui eu gostaria de focar nos Desenvolvedores e mostrar para eles como podemos usar o Azure AD para proteger nossas aplicações em processos de Autenticação e Autorização.

A proposta é fazer isso de forma manual para entendermos como os fluxos de autenticação funcionam, e para entender como um Servidor de Identidade Oauth 2.0 funciona, nada como as boas e velhas requisições HTTP puras, padrão dos protocolos citados acima.

 

Protegendo uma API de Backend

 

A princípio vamos usar o método AddMicrosoftIdentityWebApi que está no pacote Microsoft.Identity.Web, vamos fazer isso para que nosso backend seja protegido e as requisições feitas para ele devam possuir um token de Acesso. No final do artigo eu mostro como validar um token manualmente.

Na classe Startup.cs método ConfigureServices inserimos o código abaixo;

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
          .AddMicrosoftIdentityWebApi(Configuration, "AzureAd");

 

Autenticação

 

A autenticação é o processo de provar que você é quem diz ser. A plataforma de identidade da Microsoft usa o protocolo OpenID Connect para processar a autenticação.

 

Autorização

 

Autorização é o ato de conceder a uma parte autenticada a permissão para fazer algo. Ele especifica quais dados você tem permissão para acessar e o que pode fazer com esses dados. A plataforma de identidade da Microsoft utiliza o protocolo OAuth 2.0 para processar a autorização.

 

Fluxo de código de autorização (Authorization code flow)

 

A plataforma de identidade da Microsoft divide as coisas em dois tipos de fluxo, o fluxo de usuário e o fluxo de aplicação, no primeiro, temos um usuário logado para fornecer suas credenciais e aprovar consentimentos e no segundo temos aplicações falando diretamente umas com as outras onde o administrador do diretório quem aprova os consentimentos.

Vamos usar o fluxo de usuário Authorization Code Flow para começar a entender os endpoints envolvidos no processo de autorização e autenticação. Na aplicação MVC cliente vamos negociar um token de acesso para consumir uma API REST protegida, para isso precisamos obter um código através desta url.

https://login.microsoftonline.com/779811d8-4753-4c34-baeb-6b53957d52e3/oauth2/v2.0/authorize

79811d8–4753–4c34-baeb-6b53957d52e3 é meu tenantId

O segredo desse processo é que não se trata de uma requisição assíncrona, precisamos redirecionar a aplicação para essa url saindo da nossa aplicação e indo para o ambiente do Azure AD.

Com o código abaixo, fazemos um redirecionamnto para o SSO do Azure AD.

public async Task<IActionResult> Index()
{
    var scope = WebUtility.UrlEncode("openid offline_access api://13ae8188-43fa-4da1-86b6-cff6216d624c/backad");
    var url = "https://login.microsoftonline.com/779811d8-4753-4c34-baeb-6b53957d52e3/oauth2/v2.0/authorize" + "?" +
        "client_id=1ce9fb1e-835d-4020-a5ae-04695fdd5e34&" +
        "redirect_uri=https://localhost:44343/home/processCode&" +
        "response_type=code&" +
        "scope=" + WebUtility.UrlEncode("openid offline_access api://13ae8188-43fa-4da1-86b6-cff6216d624c/backad"); + "&" +
        "response_mode=query" + "&" +
        "state=12345&" +
        "nonce=xyz";
    
    return Redirect(url);
}

Esse código vai redirecionar a aplicação para a tela SSO do AD e depois vai chamar o método processCode da controller home.

public async Task<IActionResult> processCode(string code)
{

  //Obter Token Azure AD Autorization Code Flow
  var clientToken = new HttpClient();
  clientToken.DefaultRequestHeaders.Clear();
  var paramsUrl = new Dictionary<string, string>() {

      {"client_id",_clientId},
      {"scope" , _scopeAPI},
      {"redirect_uri" , "https://localhost:44343/home/processCode" },
      {"grant_type" , "authorization_code" },
      {"client_secret" , _client_secret },
      {"code" , code },

  };

  var url = $"https://login.microsoftonline.com/{_tenantId}/oauth2/v2.0/token";
  var requestrequest = new HttpRequestMessage(HttpMethod.Post, url)
  {
      Content = new FormUrlEncodedContent(paramsUrl)
  };
  var res = clientToken.SendAsync(requestrequest).Result;
  var dataMyApi = res.Content.ReadAsStringAsync().Result;
  var resultMyApi = System.Text.Json.JsonSerializer.Deserialize<ModelBasic>(dataMyApi);
  
  ...
  
  return View();
            
}

Este método receberá pela queryString o parâmetro Code, com ele realizamos o próximo passo que é a obtenção do token de acesso.

 

Url de obtenção do token

 

Acho que precisamos entender que existem diversos tipos de token quando falamos Oauth2, o mais conhecido é o token de acesso, usado em associação com algum escopo ele dará acesso em algum recurso protegido, como uma API ou parte dela por exemplo.

Mas também temos o id_token usado nos processos de sign-in e quando queremos dados do usuário logado, e o refresh_token usado quando o token de acesso expira ou quando precisamos de tokens para diferentes escopos de recursos.

Para o retorno do IDP conter o id_token e o refresh_token precisamos passar os escopos correspondentes.

  1. openid para o id_token
  2. offline_access para o refresh_token
https://login.microsoftonline.com/779811d8-4753-4c34-baeb-6b53957d52e3/oauth2/v2.0/token
//Obter Token Azure AD Client Credencial
var clientToken = new HttpClient();
var paramsUrl = new Dictionary<string, string>() {

    {"client_id","1ce9fb1e-835d-4020-a5ae-04695fdd5e34"},
    {"client_secret" , "kLi7Q~nbIZAD3nEQ2qus~s2_ljZC72XVYA-YN" },
    {"grant_type" , "authorization_code" },
    {"scope" , "openid offline_access api://13ae8188-43fa-4da1-86b6-cff6216d624c/backad" },
    {"code" , code },
    {"redirect_uri" , "https://localhost:44343/home/processCode" }
};

var url = "https://login.microsoftonline.com/779811d8-4753-4c34-baeb-6b53957d52e3/oauth2/v2.0/token";
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
    Content = new FormUrlEncodedContent(paramsUrl)
};
var res = clientToken.SendAsync(request).Result;
var data = res.Content.ReadAsStringAsync().Result;
var result = System.Text.Json.JsonSerializer.Deserialize<ModelBasic>(data);

Resposta completa usando os escopos acima.

{
    "token_type": "Bearer",
    "scope": "api://13ae8188-43fa-4da1-86b6-cff6216d624c/backad",
    "expires_in": 4518,
    "ext_expires_in": 4518,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImpTMVhvMU9XRGpfNTJ2YndHTmd2UU8yVnpNYyIsImtpZCI6ImpTMVhvMU9XRGpfNTJ2YndHTmd2UU8yVnpNYyJ9.eyJhdWQiOiJhcGk6Ly9hM2JjOWI0My05MzA5LTQ3M2YtYjRmNS1jZjJhNTg3OWJhYTciLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83Nzk4MTFkOC00NzUzLTRjMzQtYmFlYi02YjUzOTU3ZDUyZTMvIiwiaWF0IjoxNjU0NTMzODA0LCJuYmYiOjE2NTQ1MzM4MDQsImV4cCI6MTY1NDUzODYyMywiYWNyIjoiMSIsImFpbyI6IkFXUUFtLzhUQUFBQVJLbVEvRlBweUVqNUdTd1NxUlV2cGVxMTRqY1lhK3FzdTJvRXhOR2tpYnZQNlJWRlVPaFQ5d2x3Z242Z2NJRGdtV3lmSjBYanFHU3NnYnQzK1ZLd2paNnpzV2NEV05TMUh4eCtQUjFLQ3pINzFMYTlPWUpHVm5NcTNuTUFWTm1oIiwiYW1yIjpbInB3ZCJdLCJhcHBpZCI6IjUyZTFiNjg2LTNkZjYtNDNiZi1iYmY0LTNiNjFiNjc0NDc0ZiIsImFwcGlkYWNyIjoiMSIsImVtYWlsIjoid2lsc29uc2FudG9zbmV0QGdtYWlsLmNvbSIsImZhbWlseV9uYW1lIjoid2lsc29uc2FudG9zbmV0IiwiZ2l2ZW5fbmFtZSI6IndpbHNvbnNhbnRvc25ldCIsImlkcCI6ImxpdmUuY29tIiwiaXBhZGRyIjoiMTg5LjMyLjIwMC4yIiwibmFtZSI6IndpbHNvbnNhbnRvc25ldCIsIm9pZCI6IjQ5ZjlmYmNkLTFmYzUtNDQzZS1iMjExLTQ5YWQ3ZTI0NzVjNyIsInJoIjoiMC5BVFFBMkJHWWQxTkhORXk2NjJ0VGxYMVM0ME9idktNSmt6OUh0UFhQS2xoNXVxYzBBSFkuIiwic2NwIjoiQWNjZXNzQVBJIiwic3ViIjoibTdGeEhoa3YzTmhXdUtVNGdjSGQtU1BVdjd4Y3lSdUNKS3VTUm5ybkYzNCIsInRpZCI6Ijc3OTgxMWQ4LTQ3NTMtNGMzNC1iYWViLTZiNTM5NTdkNTJlMyIsInVuaXF1ZV9uYW1lIjoibGl2ZS5jb20jd2lsc29uc2FudG9zbmV0QGdtYWlsLmNvbSIsInV0aSI6Im5yUU5yV1JadFUyQW5FeC1mbGRUQUEiLCJ2ZXIiOiIxLjAifQ.DF4A0SsiGo_BAAkszukkTMvnAfqCnFIbLU88aCqugBXB9IeW7MtYijL6_-R62PLczoujeio8IsubqkaEC8YtMLo8jA52NbKlJKAp1oCLPDSw9Y_m8Re-CD_6dWEDKLVXfRujbueXpV8PZZgkLF8K4YW-28SMFFX3BZylOS83bInWquok_d31qSde2jo571Se6IS-6nv90hAZ_po38jpa9NhJJu63IF4JzrFQtvc5kHNLvSymwhEf2ZE1OtipCHqkFe720ssF60n1Z0kdbzaTJkzr6i6_Yal_0ga_Qt0608fKVK5_zAVO5QsaQL1zN4yv0Shc3xHJldYbFvFCjS9QNw",
    "refresh_token": "0.ATQA2BGYd1NHNEy662tTlX1S44a24VL2Pb9Du_Q7YbZ0R080AHY.AgABAAEAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P9-XXDmXq6NwgocdECmvo20Z2KiP4RDW_ROSTvixJnc-T5YFRY1TfPkhYQ-e0e_CorBV6vDWW7nQbeHydiU-PUDD6iOExFnjY_83-Q_neBRVDD6HYCaWmODRpWuymha2Iut7riRHwpBnODpiglMjnTxfHSpPkHOg8l9tCBQGpsLvS40kxQAjEbHcK3XNoDXeSrX8mqvqyD-RH5SB_7MLRESYV7GKXgzF1IKu3EZfroK6TCUxaJxTofYcv3Isx9QDH2eaT_dPk703wB60a0InJlwaCn6oN9sjVNq3_vVu85vDW17tWIqfNt19Q2nOaSvAqZk-7f5RInkB7vVTq5UeyodOOqURplIP49ABnRFN399FjSqBLRbLL_p_ZXzuMzq8dYVJSDbihJinMCEiFB9JvKyj0gJvogqpxClpazzolrJZnawwAWgHJHqlaa_IHrjebeAjxGaCJ_UIUm0PVEGFYnaMcxsNiV4y0lhGMwYDwhVp4BvsxWVjbNlf5xQgqF3D_Ucb9IDRJU_W5AuOKyYvYOmtMFbJHZ1g2DsyXtgNoiKEtBhN-Y7Hu0wtoYJG07BkBTAONPzWVI4Ve24h5dwCbFEpxI3ppDmwUBtgDL1L_VrmwnhAwurFDBFXOV1QyMuomlc-YxtayEVnu4sOmQ9MvABOqgvTi5K0rRRJ1XgnVEFoCJcqHGM9Q1ERlov_dTtN8ma_nKcOuvUZrhOc0TVCFairH9RSoZpcDYFc89rTuL7_mjCjXZ1-OnLJolhXVnXrAsv1eYCzu--KRWzQ5qYml27a-e0NQv2OqLkjTXe3Es1n9rfDDBxioLzWhlUgQ2_MpJoHTPbquCk1f6U4k2zLoSw8UqZRWjji8HSowZJkZknGYNwjXBCvU9S0ilKe-MLZBiXrruaRReHBg2KZcfqDyw00JPOkGDyVEwqenGRs9bD84ZdOK_7MUfTD82j6eaxLcQi1FrOitddF49ZvLWWy_QvYjJHw6gv2XfY2hUiziYurHlI25Se13v5caART-72DsyXmlGv499zh5mMz8Pq8-jyL3q8AWQgP-VmuBXnvFwDxTmShfUtBEcQLN8esENN4bVmPfqrXrytmCnIZwRFGtqcCxGp8V44VEOJRrfUmrlhz1B3g-YS3jrSIW8sb9OXlsPcfAvUr740Vk_8tAX9dSrr4G3iS62i8oGrUNSC8IYNRZMxDgpAfUfLINlJ0KeYlb3JoRbUpBO4ASLq6BhNy4zoZhBQBEXsARySpM26WUIlv3Mq06y387X5f4zbLpEsr1RRBKG6bWZlFLRAM5ql4AmbrEE_YxSFaFMvKSjCdYtikKtqyLXsWVOswvzsxMvczq8jBGZYb1vtk9JJqyKQUmJI8Kd437-ab9QHUGCS-9xWBl6Zw6xQbiR6jU6QnXnpJa2msh4ZWx1MOdv2nXrG-QgeWix8drArTwi6v-H_ebn6EJhWQM1XDMxIqybxroZvujm4kXMsK1p6GvdoCeIG9u41Y0Cy_HxByA1Q-QLM7J4",
    "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImpTMVhvMU9XRGpfNTJ2YndHTmd2UU8yVnpNYyJ9.eyJhdWQiOiI1MmUxYjY4Ni0zZGY2LTQzYmYtYmJmNC0zYjYxYjY3NDQ3NGYiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vNzc5ODExZDgtNDc1My00YzM0LWJhZWItNmI1Mzk1N2Q1MmUzL3YyLjAiLCJpYXQiOjE2NTQ1MzM4MDQsIm5iZiI6MTY1NDUzMzgwNCwiZXhwIjoxNjU0NTM3NzA0LCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC85MTg4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQvIiwibm9uY2UiOiJ4eXoiLCJyaCI6IjAuQVRRQTJCR1lkMU5ITkV5NjYydFRsWDFTNDRhMjRWTDJQYjlEdV9RN1liWjBSMDgwQUhZLiIsInN1YiI6IkZ1VFFxZjZqWUVqd3gzT1NEdEVwazZPbktuS2tKQkFKd29jSHNnbXRMdzgiLCJ0aWQiOiI3Nzk4MTFkOC00NzUzLTRjMzQtYmFlYi02YjUzOTU3ZDUyZTMiLCJ1dGkiOiJuclFOcldSWnRVMkFuRXgtZmxkVEFBIiwidmVyIjoiMi4wIn0.Dl-fZrT8ezm1zRd5tjb-wdlTXJzxucH5XBiCBOLhn0PB-vN-xl554Yl1FdTvWhssoW3l5mmVLCRK--JWkZzik5T6W-wF4TkSTEUrsOnK-zjVImHiclYXYAj1RkSlPVfd6zklcvE-jRcPdLuRttCZQ2HWWym-AoH2STmJm5P1J3wxRn7trSbDJJmLa4n5plEDXxc3U9yQaQtKcEpmI0TzOSd-_BheZEOCi-6znrQwy7zoUP0tGFqUh5HIVTaV9BDPbDOYBAd8nuTj7IAvIQk20tVJJQNr6cS1c_ptijrt20u5BbzAB2sO-Lig3F4R-mj1F1BkOMpaxzRwx1tpLLJ2Sw"
}

Resposta básica apenas com o Access token.

{
    "token_type": "Bearer",
    "scope": "api://13ae8188-43fa-4da1-86b6-cff6216d624c/backad",
    "expires_in": 4516,
    "ext_expires_in": 4516,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImpTMVhvMU9XRGpfNTJ2YndHTmd2UU8yVnpNYyIsImtpZCI6ImpTMVhvMU9XRGpfNTJ2YndHTmd2UU8yVnpNYyJ9.eyJhdWQiOiJhcGk6Ly9hM2JjOWI0My05MzA5LTQ3M2YtYjRmNS1jZjJhNTg3OWJhYTciLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83Nzk4MTFkOC00NzUzLTRjMzQtYmFlYi02YjUzOTU3ZDUyZTMvIiwiaWF0IjoxNjU0NTMzOTI0LCJuYmYiOjE2NTQ1MzM5MjQsImV4cCI6MTY1NDUzODc0MSwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhUQUFBQVJ6WmxVUmNrTlRpZVQvc3Jvc3kyQjVhZnpQSWI4WFNmaW43cDZId0tTejQra3VJMU5SMXY3ejRvZUppbDE3Yk9pQWdaaWdTdGNtWVFUZHlXL0RIQ2xsbTBOSkFtSjFuUm1UbE5rTmx1a044PSIsImFtciI6WyJwd2QiXSwiYXBwaWQiOiI1MmUxYjY4Ni0zZGY2LTQzYmYtYmJmNC0zYjYxYjY3NDQ3NGYiLCJhcHBpZGFjciI6IjEiLCJlbWFpbCI6IndpbHNvbnNhbnRvc25ldEBnbWFpbC5jb20iLCJmYW1pbHlfbmFtZSI6IndpbHNvbnNhbnRvc25ldCIsImdpdmVuX25hbWUiOiJ3aWxzb25zYW50b3NuZXQiLCJpZHAiOiJsaXZlLmNvbSIsImlwYWRkciI6IjE4OS4zMi4yMDAuMiIsIm5hbWUiOiJ3aWxzb25zYW50b3NuZXQiLCJvaWQiOiI0OWY5ZmJjZC0xZmM1LTQ0M2UtYjIxMS00OWFkN2UyNDc1YzciLCJyaCI6IjAuQVRRQTJCR1lkMU5ITkV5NjYydFRsWDFTNDBPYnZLTUprejlIdFBYUEtsaDV1cWMwQUhZLiIsInNjcCI6IkFjY2Vzc0FQSSIsInN1YiI6Im03RnhIaGt2M05oV3VLVTRnY0hkLVNQVXY3eGN5UnVDSkt1U1Jucm5GMzQiLCJ0aWQiOiI3Nzk4MTFkOC00NzUzLTRjMzQtYmFlYi02YjUzOTU3ZDUyZTMiLCJ1bmlxdWVfbmFtZSI6ImxpdmUuY29tI3dpbHNvbnNhbnRvc25ldEBnbWFpbC5jb20iLCJ1dGkiOiI2WkF5TlVzcjFFaVZQbnZhTmtGckFBIiwidmVyIjoiMS4wIn0.CV-gFLiMWBAXDBB2y3AoE2ajU7I349o8w_q0aCT_kdzjJuVtyCed_e3inuH-0Xnl5knzerc8NQscNI2YtcTd_S15YOeB6dZcpObbwEHFi_cmmWy6g8Ft46F71Qj5jSKWkdSqZzX3Z_jq0LfW3ehyzA-9s-AQd3ta-WE1xgZc2N09Y60n_3fgbr45TRwic2_JV6Yl9XVhpy4dEwhyEALMuTIYPBWcq1GYoqAQpx6eZ-lYubRGAId8bk5HdRkX0HSNVxU976nZWTDDumdIRhsNoKObDX012I6G7OaL9iU71_qTzxzuLiQXistbyqV4hn_KPKd_NYB96bWuqijaA16Vvg"
}

Agora basta passar o token para as chamadas do backend.

// Chamada API com token Bearer
using (HttpClient httpClient = new HttpClient())
{
  var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:44316/api/WeatherForecast");
  request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.access_token);
  HttpResponseMessage responseApi = httpClient.SendAsync(request).Result;
  responseApi.EnsureSuccessStatusCode();
  string responseBody = responseApi.Content.ReadAsStringAsync().Result;
  Console.WriteLine("Response:");
  Console.WriteLine(responseBody);
}

 

Registrando as aplicações no Azure AD

 

Você deve ter percebido no código acima uma propriedade chamada client_id, esse identificador representa a sua aplicação no Azure AD, então precisamos fazer um registro na plataforma para obtermos esse id e definirmos detalhes de cada aplicação como url de retorno, Client e Secret, Escopos, Roles, etc.

Modelo de Aplicações registradas no AD usadas para os exemplos abaixo

Modelo de aplicações registradas no Azure AD

  1. No menu Registro de Aplicações clique em Novo Registro
  2. Cria uma aplicação para representar seu backend
  3. Repita esse passo e crie uma aplicação para representar seu cliente
  4. Anote o Id do Aplicativo (client_id) de cada aplicação
  5. A aplicação Cliente precisa de uma (client_secret) Segredo , faça isso no menu Certificados e Segredos, Novo segredo do Cliente na aba segredos do cliente
  6. Agora vamos na aplicação servidora e vamos adicionar as Funções (Roles) no menu Funções do Aplicativos, essas funções são para usuários
  7. Além das funções vamos no menu Expor uma API e vamos expor um ponto de extremidade de API e um escopo que representa a permissão de acesso nessa aplicação
  8. Agora na aplicação cliente vamos em Permissões de APIs e clicamos em adicionar uma Permissão, escolhemos Minhas APIs e em aplicações delegadas escolhemos nossas permissões
  9. Damos consentimento de Administrador nessa relação
  10. Para finalizar a configuração vamos agora acessar o Aplicativo Empresarial Relacionado a essa App, elas tem o mesmo clientId e o mesmo nome, depois de entrar nela vamos até o menu Usuários e Grupos e associamos as funções dessa App à um usuário ou grupo.

OBS. use o Desenho acima para entender quais as aplicações precisam ser registradas no Azure AD e qual a relação entre elas.

O clientId Configurado aqui está definido como uma plataforma web e precisamos cadastrar as seguintes urls de retorno.

https://localhost:44343/home/processCode

Outro ponto importante é a definição da plataforma, pois como se trata de uma aplicação web devemos passar o Client Secret, caso estejamos trabalhando com uma aplicação SPA ou aplicações Nativas esse parâmento não deve ser passado devido a impossibilidade de armazenar essa credencial com segurança no browser. Uma vez obtido o token de acesso, basta passá-lo no header para a API;

 

Fluxo de credenciais de cliente (Client Credential)

 

Esse tipo de fluxo é comumente usado para interações de servidor para servidor que devem ser executadas em segundo plano, sem interação imediata com um usuário. Esses tipos de aplicativos geralmente são chamados de daemons ou contas de serviço

 

URL de obtenção do token

 

https://login.microsoftonline.com/779811d8-4753-4c34-baeb-6b53957d52e3/oauth2/v2.0/token
//Obter Token Azure AD Client Credencial
var clientToken = new HttpClient();
var paramsUrl = new Dictionary<string, string>() {

    {"client_id","79f45824-3df9-4772-abcc-6cb1b9557bee"},
    {"client_secret" , "bsz7Q~jKS6S2yQWBs7YdqVYLdtCzWqM1owCA." },
    {"grant_type" , "client_credentials" },
    {"scope" , "api://3dbc5a70-246a-4f03-ac06-ea1a501dcc48/.default" }
};

var url = "https://login.microsoftonline.com/779811d8-4753-4c34-baeb-6b53957d52e3/oauth2/v2.0/token";
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
    Content = new FormUrlEncodedContent(paramsUrl)
};
var res = clientToken.SendAsync(request).Result;
var data = res.Content.ReadAsStringAsync().Result;
var result = System.Text.Json.JsonSerializer.Deserialize<ModelBasic>(data);

 

Chamada da API usando o token

 

// Chamada API com token Bearer
using (HttpClient httpClient = new HttpClient())
{
    var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:44316/api/WeatherForecast");
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.access_token);
    HttpResponseMessage responseApi = httpClient.SendAsync(request).Result;
    responseApi.EnsureSuccessStatusCode();
    string responseBody = responseApi.Content.ReadAsStringAsync().Result;
    Console.WriteLine("Response:");
    Console.WriteLine(responseBody);
}

 

Obter token para um escopo de um recurso diferente

 

Esse processo nada mais é do que um processo de refresh_token, então batemos nos mesmos endpoints de token mas não passamos o code mas sim o refresh_token, isso pode ser feito por conta da expiração do token, mas também pode ser usado para se obter um token de escopos diferentes como por exemplo o MS Graph.

https://graph.microsoft.com/User.Read

O endpoint para se obter o token é o mesmo do primeiro.

https://login.microsoftonline.com/779811d8-4753-4c34-baeb-6b53957d52e3/oauth2/v2.0/token

A implementação final fica assim:

//Obter Refresh Token Azure AD Authorization Code Flow
var clientToken2 = new HttpClient();
var paramsUrl2 = new Dictionary<string, string>() {

    {"client_id","53ebfdf3-c8ec-40c5-b922-db3fc7c72bb6"},
    {"client_secret" , "kLi7Q~nbIZAD3nEQ2qus~s2_ljZC72XVYA-YN" },
    {"grant_type" , "refresh_token" },
    {"scope" , "https://graph.microsoft.com/User.Read" },
    {"refresh_token" , resultMyApi.refresh_token},
};

var url2 = "https://login.microsoftonline.com/779811d8-4753-4c34-baeb-6b53957d52e3/oauth2/v2.0/token";
var request2 = new HttpRequestMessage(HttpMethod.Post, url2)
{
    Content = new FormUrlEncodedContent(paramsUrl2)
};
var res2 = clientToken2.SendAsync(request2).Result;
var dataMyApi2 = res2.Content.ReadAsStringAsync().Result;
var resultMyApi2 = System.Text.Json.JsonSerializer.Deserialize<ModelBasic>(dataMyApi2);

Com esse novo token agora posso acessar a api MS graph.

https://graph.microsoft.com/v1.0/users/{unique_name | oid}

ou

https://graph.microsoft.com/v1.0/me

Agora é só chamar o Graph e passar o novo token.

//Chamada Graph com token Bearer para usuário logado
var resultGraphUser = new GraphUser();
var urlGraphUser = $"https://graph.microsoft.com/v1.0/me";
using (HttpClient userClient = new HttpClient())
{
    var requestUser = new HttpRequestMessage(HttpMethod.Get, urlGraphUser);
    requestUser.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken2);
    HttpResponseMessage responseUser = userClient.SendAsync(requestUser).Result;
    responseUser.EnsureSuccessStatusCode();
    var dataGraphUser = responseUser.Content.ReadAsStringAsync().Result;
    resultGraphUser = System.Text.Json.JsonSerializer.Deserialize<GraphUser>(dataGraphUser);

}

 

Autorização no backend

 

Nosso token de acesso conta com uma claim chamada Roles, nela teremos a associação das Roles atribuídas na Aplicação e no usuário através do passo 10 do registro de aplicação acima. Com essas claims podemos aumentar a flexibilidade do processo de autorização, chegando ao nível da linha de código da aplicação. vamos observar um token aberto usando o site https://jwt.ms

{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "jS1Xo1OWDj_52vbwGNgvQO2VzMc",
  "kid": "jS1Xo1OWDj_52vbwGNgvQO2VzMc"
}.{
  "aud": "api://13ae8188-43fa-4da1-86b6-cff6216d624c",
  "iss": "https://sts.windows.net/76a0e64d-44fe-4b10-85f3-424fb91868dd/",
  "iat": 1654628628,
  "nbf": 1654628628,
  "exp": 1654632594,
  "acr": "1",
  "aio": "ATQAy/8TAAAAng7efiuG1HLj/kGnUPWgQ8D9//prJfrW/Rh/V17V9bNX9xnEQkkquU2RFry8O0DH",
  "amr": [
    "pwd"
  ],
  "appid": "1ce9fb1e-835d-4020-a5ae-04695fdd5e34",
  "appidacr": "1",
  "family_name": "01",
  "given_name": "usuario01",
  "ipaddr": "186.207.77.86",
  "name": "usuario01",
  "oid": "89206ab3-a495-45b4-a023-bb9fdf493f5c",
  "rh": "0.AX0ATeagdv5EEEuF80JPuRho3YiBrhP6Q6FNhrbP9iFtYkyaAAY.",
  "roles": [
    "Contributor"
  ],
  "scp": "backad",
  "sub": "TSXsch8CHZCIQB2_E6ByU-7JXC0CnS2HDDSxCuxz-so",
  "tid": "76a0e64d-44fe-4b10-85f3-424fb91868dd",
  "unique_name": "usuario01@tdcpocazuread.onmicrosoft.com",
  "upn": "usuario01@tdcpocazuread.onmicrosoft.com",
  "uti": "i2owjjfiKkmzvA9Tzr6IAA",
  "ver": "1.0"
}.[Signature]

Quando protegemos o backend com o Microsoft.Identity.Web teremos os dados do token integrados ao usuário Principal do contexto do asp.net. Sendo assim basta utilizar classes como User método IsInRole, ou classes de atributos como Authorize para validar o acesso.

[HttpGet]
public IActionResult Get()
{

  if (User.IsInRole("Contributor"))
  {
      var rng = new Random();
      return Ok(Enumerable.Range(1, 5).Select(index => new WeatherForecast
      {
          Date = DateTime.Now.AddDays(index),
          TemperatureC = rng.Next(-20, 55),
          Summary = Summaries[rng.Next(Summaries.Length)]
      })
      .ToArray());
  }

  return Forbid();
}

também podemos fazer isso para a controller toda.

[Authorize(Roles ="Contributor")]
public class WeatherForecastController : ControllerBase...

 

Validando um Token manualmente

 

Um JWT é uma String composta, que é separada por um ponto. O primeiro segmento é conhecido como cabeçalho, o segundo como corpo e o terceiro como assinatura. A assinatura é usada para validar o token.

//Pegar os segmentos do token  
var allParts = token.Split(".");  
var header = allParts\[0\];  
var payload = allParts\[1\];  
var signature = allParts\[2\];

Os tokens emitidos pelo Azure AD são assinados usando algoritmos de criptografia assimétrica padrão do setor, como RS256. O cabeçalho do JWT contém informações sobre a chave e o método de criptografia usados para assinar o token:

{  
  "typ": "JWT",  
  "alg": "RS256",  
  "x5t": "jS1Xo1OWDj\_52vbwGNgvQO2VzMc",  
  "kid": "jS1Xo1OWDj\_52vbwGNgvQO2VzMc"  
}

Podemos ver na url de .well-known quais são os algoritmos de assinatura suportados, para isso basta acessar a URL de metadados do Azure AD https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration

{
    "token_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
    "token_endpoint_auth_methods_supported": [
        "client_secret_post",
        "private_key_jwt",
        "client_secret_basic"
    ],
    "jwks_uri": "https://login.microsoftonline.com/common/discovery/v2.0/keys",
    "response_modes_supported": [
        "query",
        "fragment",
        "form_post"
    ],
    "subject_types_supported": [
        "pairwise"
    ],
    "id_token_signing_alg_values_supported": [
        ***"RS256"***
    ],
    "response_types_supported": [
        "code",
        "id_token",
        "code id_token",
        "id_token token"
    ],
    "scopes_supported": [
        "openid",
        "profile",
        "email",
        "offline_access"
    ],
    "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0",
    "request_uri_parameter_supported": false,
    "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo",
    "authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
    "device_authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode",
    "http_logout_supported": true,
    "frontchannel_logout_supported": true,
    "end_session_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/logout",
    "claims_supported": [
        "sub",
        "iss",
        "cloud_instance_name",
        "cloud_instance_host_name",
        "cloud_graph_host_name",
        "msgraph_host",
        "aud",
        "exp",
        "iat",
        "auth_time",
        "acr",
        "nonce",
        "preferred_username",
        "name",
        "tid",
        "ver",
        "at_hash",
        "c_hash",
        "email"
    ],
    "kerberos_endpoint": "https://login.microsoftonline.com/common/kerberos",
    "tenant_region_scope": null,
    "cloud_instance_name": "microsoftonline.com",
    "cloud_graph_host_name": "graph.windows.net",
    "msgraph_host": "graph.microsoft.com",
    "rbac_url": "https://pas.windows.net"
}

Os tokens são codificados com um padrão chamado Base64Url, para poder ler os segmentos de um token precisamos decodificar esse padrão. Eu usei a classe Base64UrlEncoder da namespace Microsoft.IdentityModel.Tokens

Depois de decodificar o token, basta serializar para um modelo, algo como abaixo:

private static string ReadKid(string header)  
{
 var uncodeBase64 = Base64UrlEncoder.Decode(header);  
 var azureKeys = Newtonsoft.Json.JsonConvert.DeserializeObject<Key>(uncodeBase64);  
 return azureKeys.kid;
}

E abaixo temos um exemplo de como Verificar a Assinatura do Token.

private static bool ValidateToken(string kid, string header, string payload, string signature)
{
    string keysAsString = null;
    const string microsoftKeysUrl = "https://login.microsoftonline.com/779811d8-4753-4c34-baeb-6b53957d52e3/discovery/keys";

    using (var client = new HttpClient())
    {
        keysAsString = client.GetStringAsync(microsoftKeysUrl).Result;
    }
    var azureKeys = Newtonsoft.Json.JsonConvert.DeserializeObject<MicrosoftConfigurationKeys>(keysAsString);
    var signatureKeyIdentifier = azureKeys.keys.FirstOrDefault(key => key.kid.Equals(kid));
    if (signatureKeyIdentifier != null)
    {
        var signatureKey = signatureKeyIdentifier.x5c.First();
        var certificate = new X509Certificate2(Convert.FromBase64String(signatureKey));
        var rsa = certificate.GetRSAPublicKey();
        var data = Encoding.ASCII.GetBytes(string.Format("{0}.{1}", header, payload));

        var signatureBytes = Base64UrlEncoder.DecodeBytes(signature);
        var isValidSignature = rsa.VerifyData(data, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        return isValidSignature;
    }

    return false;
}

As urls de jwks_uri, fornecem a localização do conjunto de chaves públicas que correspondem às chaves privadas usadas para assinar tokens. A chave da Web JSON (JWK) localizada no jwks_uricontém todas as informações da chave pública em uso naquele momento específico. O formato JWK é descrito na RFC 7517 . Seu aplicativo pode usar a kiddeclaração no cabeçalho JWT para selecionar a chave pública deste documento, que corresponde à chave privada que foi usada para assinar um token específico. Ele pode então fazer a validação de assinatura usando a chave pública correta e o algoritmo indicado.

A qualquer momento, o Azure AD pode assinar um id_token usando qualquer um de um determinado conjunto de pares de chaves público-privadas. O Azure AD gira o possível conjunto de chaves periodicamente, portanto, seu aplicativo deve ser escrito para lidar com essas alterações de chave automaticamente. Uma frequência razoável para verificar atualizações nas chaves públicas usadas pelo Azure AD é a cada 24 horas.

Nesses casos o processo de validação das roles, ou seja, a parte da autorização, precisa extrair a claim Roles do token e fazer algo parecido com o processo acima, mas lembrando que validando o token na mão sem a configuração da classe Startup e o uso da biblioteca Microsoft.Identity.Web não teremos os dados do token integrados ao usuário Principal do contexto do asp.net. Sendo assim precisaremos fazer isso na mão também.

Para isso basta fazer algo parecido com o método ReadKid que vai decodificar o payload e extair as roles para uma lista de strings, observe o modelo abaixo

public class PayLoad {
  public List<string> Roles { get; set; }
}

agora podemos montar o método para ler as Roles, mais ou menos assim:

private static List<string> ReadRoles(string payload)
{
  var uncodeBase64 = Base64UrlEncoder.Decode(payload);
  var azureKeys = Newtonsoft.Json.JsonConvert.DeserializeObject<PayLoad>(uncodeBase64);
  return azureKeys.Roles;
}

E pronto agora podemos validar.


var allParts = token.Split(".");
var header = allParts[0];
var payload = allParts[1];
var signature = allParts[2];

var roles = ReadRoles(payload);

if (roles.Contains("Contributor"))
{ 
   //OK
}

 

Referências

  1. https://docs.microsoft.com/pt-br/azure/active-directory/develop/v2-oauth2-auth-code-flow

  2. https://docs.microsoft.com/pt-br/azure/active-directory/develop/quickstart-register-app

  3. https://docs.microsoft.com/pt-br/azure/active-directory/develop/access-tokens#validating-the-signatu...

Co-Authors
Version history
Last update:
‎Jun 14 2022 09:04 AM
Updated by: