IdentityServer i odświeżanie wartości claims

blog.gutek.pl 3 lat temu

Kiedyś czytałem o DBA pracującym w jednym z banków. Przychodził on codziennie rano do pracy, przeważnie wcześnie, przed innymi. Robił sobie kubek gorącej kawy. Włączał monitor i komputer, kładł kurtkę na ramionach krzesła. Gorącą kawę stawiał przy klawiaturze. Kiedy inni przychodzili widzieli biurku osoby „pracującej”. To co zaś robił nasz główny bohater, to wychodził z pracy, szedł dosłownie na drugą stronę ulicy do innego banku. Logował się tam do swojego komputera i odstawiał podobną sztukę w drugim banku. Dwa pełne etaty, w dwóch bankach, jako DBA… żyć nie umierać ;)

Czy historia jest prawdziwa ciężko mi powiedzieć, skończyła się co prawda źle dla DBA, po kilku miesiącach go złapano i oskarżono oraz nałożono kary finansowe.

Aktualizacja Claims w ADFS

Na szczęście nie zawsze musi być tak źle, kilka lat temu pisałem o tym jak wymusić odświeżenie claims w aplikacji z wykorzystaniem ADFS. Ogólnie chodzi o to, iż w naszym systemie jedna osoba może poddawać się za kilka firm. Będąc firmą X może działać z imienia i upoważnienia tej firmy. Będąc w firmie Y robi to samo bez wglądu do danych firmy Y.

Można to tak rozwiązać jak to zrobił Teams ;) zakładać konta w danej organizacji, jednak to nie jest idealne rozwiązanie. W szczególności, iż osoba która może zmieniać firmy przeważnie może prowadzić firmę dającą pewną konkretną usługę którą musi świadczyć w imieniu firmy X lub Y. Zaś sama firma może mieć wielu pracowników robiących to.

Szukamy więc rozwiązania które umożliwi jednemu kontowi działać jako inne i ten kontekst można zmieniać. Zmiana zaś powinna być… niezauważalna i nie odczuwalna prawie dla użytkownika końcowego.

Aktualizacja Claims w IdentityServer

Jako iż mieliśmy już działające rozwiązanie i ludzie od niego przywykli ciężko jest to teraz od tak zmienić. Przy migracji do IdentityServer v4, musieliśmy więc zachować funkcjonalność opisaną w zalinkowanym artykule.

To znaczy, iż użytkownik nie powinien się musieć ponownie logować przy wykonaniu zmiany acting as. Dosłownie zmiana acting as to zmiana w bazie danych pola, które jest pobierane i następnie wrzucane jako claim.

By to zrobić, w IdentityServer trzeba przejść przez proces odświeżania tokenu. Dokładniej trzeba ustawić zmienną dla aplikacji UpdateAccessTokenClaimsOnRefresh a następnie po odświeżeniu tokenu pobrać na nowo claims dla danego profilu i zaktualizować nasze ciasteczko. W przeciwnym wypadku zmiana nie zostanie uwzględniona. Dodatkowo potrzebujemy jeszcze GrantType który umożliwi nam na odświeżenie tokenu z poziomu kodu, myśmy poszli z HybridAndClientCredentials.

Kod który umożliwia pobranie nowych claims wygląda następująco:

[Route("update-claims")] public async Task<IActionResult> UpdateClaims(string uid) { if (!User.Identity.IsAuthenticated) { return NoContent(); } var currentUid = User.FindFirst(EdenClaimTypes.UserId)?.Value ?? ""; if (!string.Equals(currentUid, uid, StringComparison.Ordinal)) { return NoContent(); } var disco = await _discoveryCache.GetAsync(); if (disco.IsError) { throw new Exception(disco.Error); } var opt = _options.Value; var rt = await HttpContext.GetTokenAsync("refresh_token"); var tokenClient = _httpClientFactory.CreateClient("discovery"); using var refreshToken = new RefreshTokenRequest { Address = disco.TokenEndpoint, ClientId = opt.ClientId, ClientSecret = opt.ClientSecret, RefreshToken = rt }; var tokenResult = await tokenClient.RequestRefreshTokenAsync(refreshToken); if (tokenResult.IsError) { return NoContent(); } var new_access_token = tokenResult.AccessToken; var new_refresh_token = tokenResult.RefreshToken; var expiresAt = ApplicationTime.Current + TimeSpan.FromSeconds(tokenResult.ExpiresIn); var info = await HttpContext.AuthenticateAsync("Cookies"); info.Properties.UpdateTokenValue("refresh_token", new_refresh_token); info.Properties.UpdateTokenValue("access_token", new_access_token); info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)); using var uir = new UserInfoRequest { Address = disco.UserInfoEndpoint, ClientId = opt.ClientId, ClientSecret = opt.ClientSecret, Token = new_access_token }; var claims = await tokenClient.GetUserInfoAsync(uir); var currentIdentity = info.Principal.Identity as ClaimsIdentity; var distinctClaimTypes = claims.Claims.Select(x => x.Type).Distinct(); foreach (var claimType in distinctClaimTypes) { var currentCount = currentIdentity.Claims.Count(x => x.Type == claimType); if (currentCount > 0) { var currentClaims = currentIdentity.Claims.Where(x => x.Type == claimType).ToList(); foreach (var currentClaim in currentClaims) { currentIdentity.RemoveClaim(currentClaim); } } currentIdentity.AddClaims(claims.Claims.Where(x => x.Type == claimType)); } await HttpContext.SignInAsync("Cookies", info.Principal, info.Properties); return NoContent(); }

Po słowie

Może kod się Tobie przyda. Jest to jednak hack i osobiście wolałbym go nie robić, zamiast tego wymusić ponowne logowanie się na użytkowniku, które można bardzo fajnie dzięki front-channel zaimplementować.

PS.: Koniecznie przeczytajcie komentarze, w szcególności Tomka – przy innych wymaganiach i innym systemie, to była by dobra droga.

Idź do oryginalnego materiału