Open christopherac90 opened 1 week 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.
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()
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
**--------------------------------**** 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; }
}
**--------------------------------**** 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
) {
}
configureCodeFlow() {
}
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); } });
} 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; }
}
}
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
Additional context
It's important, to say that I'm not using BFF yet.
Thank you for any help you can provide me.