DuendeSoftware / Support

Support for Duende Software products
20 stars 0 forks source link

Trouble exchanging the code for a token #1318

Open christopherac90 opened 1 week ago

christopherac90 commented 1 week ago

Which version of Duende.AccessTokenManagement are you using? Duende V 7.0.4

Which version of .NET are you using? Net 8.0

Describe the bug At work, were trying to migrate from Identity Server 4 to Duende7. Right now the login is working with MVC, and clients in Angular. The thing is that for company policys, all the apps should be now migrate to Angular (identity included). So We started the poject by migrating the backend, from the original IS4, to net 8, and duende 7. This API (now is an API) is the Idendity Server, and is running Ok. On the other hand, I have a project in angular (17), that it is the Loggin, this loggin, is going to call the Session method(POST) from the IS, and there is going the be performed the authentication, and then the redirection to the client. Also I have another Angular project that is going to be Client (IS Admin), this project is configured with angular-oauth2-oidc. The User when clicks a button in this app, is going to be redirected to the Login app (angular) with all its parameters on the url.

The thing is that when I run all the apps, and APIS, I enter the credentials on the loggin page, and the IS Authenticates succesfully, and then redirects to the client, with the code and everything (https:/localhost:4201#code=xxxxxx), but then nothing happens, the code is not exchenged by the token. I have tried a lot of differents things and configurations, but haven't had sucess, This is a big issue for my, because I need the token to permform differents api calls.

So my question is, should I perform the Token endpoint call mannually?

To Reproduce Here is my configuration from IDS( V7) using OAuth2.Abstracciones.SG; using OAuth2.AbstraccionesBase.Enumerados; using OAuth2.Contenedores; using OAuth2.Diagnostico; using OAuth2.Registro; using OAuth2.Configuraciones; using OAuth2.Navegador; using OAuth2.SG.Usuario; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using OAuth2.ManejoDeExcepciones; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Hosting; using OAuth2.Seguridad; using System.Security.Cryptography.X509Certificates; using Duende.IdentityServer; using System.Security.Claims; using Duende.IdentityServer.Stores.Serialization; using Duende.IdentityServer.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authentication.Cookies; using IDP.Int.WEB.API; using Duende.IdentityServer.ResponseHandling;

var builder = WebApplication.CreateBuilder(args);

var loggerFactory = new LoggerFactory();

loggerFactory.AddProvider(new ProveedorDeRegistro(new Configuracion { EventoId = new EventId(8000, OAuth2.ManejoDeExcepciones.Constantes.nombreLog), NivelDeRegistro = LogLevel.Error })); builder.Logging.AddConsole(); builder.Logging.AddDebug();

builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) .AddEnvironmentVariables();

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options => { options.User.RequireUniqueEmail = false; }).AddUserStore() .AddRoleStore() .AddDefaultTokenProviders();

builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen();

IConfiguration configuration = builder.Configuration;

builder.Services.AddHttpContextAccessor();

Certificados instanciaDeCertificados = new Certificados(StoreName.My, StoreLocation.LocalMachine);

string subject = configuration.ObtengaElSubject();

X509Certificate2 certificadoDeFirma = instanciaDeCertificados.ObtengaElCertificadoPorSubject(subject);

builder.Services.AddAuthentication()

.AddCookie("Cookies", options =>
{

    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;

});

builder.Services.AddIdentityServer(options => { options.UserInteraction.LoginUrl = "https://localhost:4200/"; options.UserInteraction.LogoutUrl = "https://localhost:4200/signout-oidc"; options.UserInteraction.ErrorUrl = "https://localhost:4200/"; options.LowerCaseIssuerUri = false; options.PersistentGrants.DataProtectData = false;

}).AddClientStore() .AddResourceStore() .AddCorsPolicyService() .AddPersistedGrantStore() .AddDeviceFlowStore() .AddSigningCredential(certificadoDeFirma, OAuth2.Constantes.Certificado.Algoritmo) .AddAuthorizeInteractionResponseGenerator() ;

builder.Services.AddScoped<IServicioUsuario, OAuth2.SG.Usuario.Usuario>(); builder.Services.AddSingleton<IServicioBitacora, OAuth2.SG.Bitacora.Registrador>(); var timeZone = configuration.ObtengaLaRutaZoneTime(); TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);

builder.Services.AddSingleton(timeZoneInfo);

builder.Services.AgregarIdentificadorGlobal(); builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); builder.Services.AddControllers(); var app = builder.Build(); Duende.IdentityServer.Stores.Serialization.ClaimConverter converter = new Duende.IdentityServer.Stores.Serialization.ClaimConverter(); JsonConvert.DefaultSettings = () => new JsonSerializerSettings { Converters = new System.Collections.Generic.List { ClaimConverter() } }; app.UseHttpsRedirection(); app.UseRouting();

string[] origenesPermitidos = configuration.ObtengaLosDominiosPermitidos();

app.UseCors(opciones => { opciones.WithOrigins(origenesPermitidos).AllowAnyMethod().AllowAnyHeader().AllowCredentials(); }); app.usarIdentificadorGlobal(); app.UseManejoDeExcepciones(configuration, loggerFactory); app.UseNavegador(); app.UseAuthentication(); app.UseIdentityServer(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); Console.Title = "Servicio de autenticación"; if (app.Environment.IsDevelopment()) {
app.UseSwagger(); app.UseSwaggerUI(); } app.Run();

**--------------------------------**** Authentication Method from IS

    public async Task<IActionResult> Sesion([FromBody] Contenedores.DatosDeUsuario elUsuario)
    {
        Parameters extraParams = new Parameters() { 
            new KeyValuePair<string, string>("Suscriptor", elUsuario.Cliente) 
        };
        var ru = new RequestUrl(elUsuario.Method);
        var url = ru.CreateAuthorizeUrl(elUsuario.Cliente, elUsuario.ResponseType, elUsuario.Scope,
            elUsuario.ReturnUrl, elUsuario.State, elUsuario.Nonce, null,null,null, elUsuario.ResponseMode,elUsuario.CodeChallenge,
            elUsuario.CodeChallengeMethod,null,null,null,null, extraParams);

        var context = await _interaction.GetAuthorizationContextAsync(url);

        //Se validan credenciales contra la fuente de la verdad BD o AD
        if (laAutenticacionDeUsuario.EstaAuthorizado)
        {
            AuthenticationProperties props = null;
            if (elUsuario.RecordarSesion)
            {
                props = new AuthenticationProperties
                {
                    IsPersistent = true,
                    ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromDays(7))
                };
            };

            //cookie de autenticación con el subject ID y el nombre de usuario
            var isuser = new IdentityServerUser(laAutenticacionDeUsuario.Subject)
            {
                DisplayName = laAutenticacionDeUsuario.Usuario
            };

            laAutenticacionDeUsuario.Claims.Add(new BcrClaimUsuario() { Llave = JwtClaimTypes.Subject, Valor = laAutenticacionDeUsuario.Subject });
            laAutenticacionDeUsuario.Claims.Add(new BcrClaimUsuario() { Llave = JwtClaimTypes.AuthenticationTime, Valor = DateTime.Now.ToEpochTime().ToString() });

            var claims = UsuarioAccesoClaims.Convierta(laAutenticacionDeUsuario.Claims);
            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            var user = new ClaimsPrincipal(identity);

             await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, user);
            this.HttpContext.User = user;
            if (context != null)
            {

                if (_interaction.IsValidReturnUrl(url) )
                {
                    return new JsonResult(new { RedirectUrl = "https://localhost:5001" + url, IsOk = true });
                }
                //return Ok(context);
            }
            };
        var vm = await BuildLoginViewModelAsync(elUsuario);
        return View(vm);
    }

**--------------------------------**** AuthorizeInteractionResponseGenerator I had to implement this class, becuse the Resuqest at this point, it was with another subject and another claims, not my the authenticated user.

using Duende.IdentityServer.Configuration; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Models; using Duende.IdentityServer.ResponseHandling; using Duende.IdentityServer.Services; using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Authentication; using IdentityModel; using static Duende.IdentityServer.IdentityServerConstants; using Microsoft.VisualBasic; using System; using Microsoft.AspNetCore.Authentication.Cookies; namespace IDP.Int.WEB.API { public class CustomAuthorizeInteractionResponseGenerator : AuthorizeInteractionResponseGenerator { private readonly IHttpContextAccessor _contextAccessor; public CustomAuthorizeInteractionResponseGenerator(IdentityServerOptions options, Duende.IdentityServer.IClock clock, ILogger logger, IConsentService consent, IProfileService profile, IHttpContextAccessor httpContextAccessor
) : base(options, clock, logger, consent, profile) { this._contextAccessor = httpContextAccessor; }

    protected  override async Task<InteractionResponse> ProcessLoginAsync(ValidatedAuthorizeRequest request)
    {
        var principal = this._contextAccessor.HttpContext;
        var authenticateResul = await _contextAccessor.HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        request.Subject = authenticateResul.Principal;
        var result = await base.ProcessLoginAsync(request);            
        return result;
    }        
}

}

**--------------------------------**** Client auth configuration:

export const authCodeFlowConfig: AuthConfig = { issuer: environment.idPUrl, redirectUri: environment.idPRedirectUri, clientId: environment.Cliente, responseType: environment.ResponseType, scope: environment.Scope, customQueryParams: { 'Suscriptor': environment.Suscriptor, 'Method': environment.Method }, silentRefreshRedirectUri: window.location.origin + '/inicio-sesion', useSilentRefresh: true, showDebugInformation: true, oidc: true, clearHashAfterLogin: false

};

In the enviroment file

idPRedirectUri: window.location.origin +'/callback', idPPostLogoutRedirectUri: window.location.origin + '/signout-oidc', ResponseType: 'code id_token', Scope: 'openid profile email roles', Suscriptor: 'Administrador de Seguridad', Cliente: 'BCR_IDP_ADMIN', //ResponseMode: 'fragment', EsExterno: false, Method: '/connect/authorize/callback',

My client flow from Angular.

import { Injectable } from '@angular/core'; import { Observable, BehaviorSubject } from 'rxjs'; import { filter, map, tap } from 'rxjs/operators'; import { OAuthService } from 'angular-oauth2-oidc'; import { ActivatedRoute, Params, Router } from '@angular/router'; import jwt_decode from "jwt-decode"; import { BnNgIdleService } from 'bn-ng-idle'; import { environment } from 'src/environments/environment'; import { authCodeFlowConfig } from '../authentication/auth.config'; import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';

@Injectable({ providedIn: 'root' }) export class SesionService { private isAuthenticatedSubject$ = new BehaviorSubject(false); isLoggedIn$: Observable = this.isAuthenticatedSubject$.asObservable(); isLoggedOut$: Observable = this.isLoggedIn$.pipe(map(isLoggedIn => !isLoggedIn)); _params: Params; constructor( public oauthService: OAuthService, private router: Router, private bnIdle: BnNgIdleService, private _activateRoute: ActivatedRoute ) {

if (this.hasValidAccessToken()) {
  this.router.navigate(['principal']);
}

}

configureCodeFlow() {

this.oauthService.configure(authCodeFlowConfig);

this.oauthService.setStorage(sessionStorage);

this.oauthService.tokenValidationHandler = new JwksValidationHandler();
this.oauthService.setupAutomaticSilentRefresh();

this.oauthService.loadDiscoveryDocumentAndTryLogin().then(()=>{

  if(!this.oauthService.hasValidAccessToken() && !this.oauthService.hasValidIdToken()){
    if(!window.location.href.includes('code=')){
        this.oauthService.initCodeFlow();
    }
  }
})
.catch(error=>{
  console.log('Error Intercambiando el código por token',error)
});

this.oauthService.events
  .subscribe(_ => {
    this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken());
  });

this.oauthService.events
  .pipe(filter(e => ['session_terminated', 'session_error', 'token_refresh_error'].includes(e.type)))
  .subscribe(e => this.logout());

}

iniciarTiempoExpiracionInactividad(timeOutSeconds: number) { this.bnIdle.startWatching(timeOutSeconds).subscribe( (isTimedOut: boolean) => { if (isTimedOut) { this.bnIdle.stopTimer(); this.logout(); } } ); }

login() { this.oauthService.loginUrl= environment.loginURL; this.oauthService.initCodeFlow();

}

logout() { this.oauthService.logOut(); }

tokenReceivedEvent() { return this.oauthService.events.pipe( filter((e: any) => e.type === 'token_received'), tap(e => { this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken()); }) ); }

hasValidAccessToken() { return this.oauthService.hasValidAccessToken(); }

getUsuarioNombre(): string { this._activateRoute.queryParams
.subscribe( params=>{ if(params.usuario != null && (localStorage.getItem('nombre_usuario') != '' || localStorage.getItem('nombre_usuario') != null)){ localStorage.setItem('nombre_usuario', params.usuario); } });

return localStorage.getItem("nombre_usuario");

} getUsuariOOficina(): string { const accessToken = this.oauthService.getAccessToken(); if (!accessToken) { return null; } const jwtDecoded = jwt_decode(accessToken); return jwtDecoded['Oficina']; }

//#eliminar solo para efectos de desarrollo getAccessToken(): string { const accessToken = this.oauthService.getAccessToken(); if (!accessToken) { return null; }

return accessToken;

}

}

Expected behavior I was expecting the client to exchange the code for a token, and then I can get the token on the client, to be used.

Log output/exception with stacktrace

The data I was getting it from DB

Additional context

It's important, to say that I'm not using BFF yet.

Thank you for any help you can provide me.

RolandGuijt commented 6 days ago

Trying to separate IdentityServer's UI from the IdentityServer project is not recommended nor is it going to work, at least not in a safe manner. One reason is that the protocol relies on cookies being set and these have to be samesite cookies to prevent Cross Site Request Forgery. The problems you are experiencing probably have to do with that.

A good way to make this work is to integrate the SPA UI in the IdentityServer project. We have an example of this here. It is using just HTML with vanilla javascript, not Angular. But if Angular is a must you can use this as a starting point.