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, że 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, że 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 że 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, że 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 za pomocą front-channel zaimplementować.

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

15 KOMENTARZE

  1. Kopiując komentarz z LIN:

    Auth is hard, Authz is even harder.

    To co pokazujesz to przykład użycia zawartości tokenu do autoryzacji i wykonywanie autoryzacji blisko IdP a nie zasobu. Rozwiązaniem na to jest zmiana podejścia i użycie chociażby bratniego dla Identitity Server Policy Server – https://policyserver.io/

    Wtedy dany endpoint, który weryfikuje użytkownika pobiera sobie z Policy Server informację o wszystkich dostępnych dla niego firmach i pozwala mu zmienić na firmę “X” czy “Y” w zależności od tej informacji, lub weryfikuję ją z z Policy Sever przy każdej operacji zmiany.

    Jak to napisane jest na główej stronie projektu – (…) Authorization is hard – and authorization is often conflated with authentication and identity. We think that tightly coupling “Identity and Access Management” in a single solution is the wrong approach. (…)

    To jest dokładnie to co miałeś zrobione w ramach swojego “hack” bo ADFS nie miał na to lepszego rozwiązania.

    PS. Uwagę o Teams z bloga pominę :) – jak już rozmawialiśmy, nie potrzebujesz nowego konta, ale tak jest łatwiej administratorom (albo o tym nie wiedzą :) )

    • Nic dodać nic ująć. Koncepcja stojąca za Policy Server’em bardzo mi się podoba. Tomku, czy miałeś może okazję korzystać z tego produktu? Jakiś czas temu bardzo mocno testowaliśmy Policy Server ale w końcu zdecydowaliśmy się na custom’ową implementację (z kilku powodów). Tym niemniej duch Policy Server’a czyli wyraźne, bardzo mocne rozdzielenie autoryzacji i uwierzytelnienia jest wciąż z nami.

      • Tam gdzie tworzyliśmy takie rozwiązanie to była własna usługa – powód:
        – status projektu i kwestia podejścia do OSS
        – przekonania architekta itp.
        – quick PoC przeszedł w produkcję :).

        Ogólnie to podejście ma sens tym bardziej, gdy masz więcej niż jedną warstwę, która wymaga autoryzacji:
        – UX
        – API przy dostępie do danych

        Ale zasada była taka sama

        • U nas jednym z powodów wybrania custom’owego rozwiązania były dość specyficzne wymagania biznesowe, do których musielibyśmy nagiąć PS, a także bardzo wysokie wymagania jeśli chodzi o wydajność.

          Tym niemniej PS ma kilka bardzo fajnych funkcji np.: wsparcie dla rozwiązań multi tenant czy hierarchie polityk (ang. policy) i tenant’ów, które działają tak, że polityki/tenant’i dzieci dziedziczą na przykład przypisania uprawnień od rodziców. Pozwala to realizować bardzo złożone scenariusze chociaż jest dość trudne do ogarnięcia.

          • “Pozwala to realizować bardzo złożone scenariusze chociaż jest dość trudne do ogarnięcia.”

            czyli armata na przybicie gwoździa :)

      • no i super jak miales takie wymagania. czy dla pojedynczego wymagania ktore jest filtrem na dane i ktore w przyszlosci moze zostac usuniete kompletenie, a i ktore nie ma planow na rozrost w ciagu nastepnych 5 lat (patrz daty postow), to dalej bys wyciagal custom implementacje PolicyServer?

        tak kompletnie szczerze?

        ja wiem ze kazdy produkt mozna zrobic w DDD tylko czy kazdy produkt pod DDD sie nadaje? A ja wiem, ze moge napisac w Java aplikacje do przeszukiwania logow, tylko czy Java bedzie naprwade dobrym rozwiazaniem skoro mozesz to zrobic w command line?

        czasami KISS jest bardzo dobrym rozwiazaniem.

    • teams – znow, latwiej nie latwiej. maja rozne polityki i na przyklad jedna polityka mowi: zakaz guest accounts. i kropka, nie zmienisz tego do poki pokolenie sie nie zmieni :)

      co do tego ze to nie jet dobre – napisalem na koncu, ze nie uwazam, ze to jest dobre.

      Co do produkty PolicySErver – archived i nie do uzytku. Tworzenie nowego, jest zbyt czasochlonne i kosztochlonne. Do tego PolicyServer w wersji platnej to armata na problem. Nie potrzebujesz armaty by przybic gwozd do sciany, ale oczywiscie zgodnie z najlepszymi checiami mozesz tez ja do tego wykorzystasc.

      i jak zreszta sam autor pisze: these “identity roles” are not typically meaningful to applications in a solution :)

      u nas role sa meaningful i sa proste. mozna to rozbijac jednak nie trzeba i nie uwazam ze powinnismy. Zas role maja sie nijak do zmiany organizacji. to nie jest powiazane.

      jezeli zas ma sie wymogi na roznego rodzaju prawa dostepowa do aplikacji, i jeszcze do tego hierarchiczne, to jaknajbardziej z czegos trzeba by bylo skorzystac.

      • “u nas role sa meaningful i sa proste” – to jest dokładnie to co w PS nazywa się “application role”. Jeśli Twój IdentitySrv (czy czego tam używasz) zwraca “identity role” to PS ma możliwość przemapowania ich na “application role”.

        Tak z ciekawości. Bo z jednej strony napisałeś “napisalem na koncu, ze nie uwazam, ze to jest dobre.”, a z drugiej “mozna to rozbijac jednak nie trzeba i nie uwazam ze powinnismy”. Jak Twoim zdaniem powinno wyglądać “koszerne” rozwiązanie w Twoim przypadku?

        • ale po co koszerne rozwiaznie? Po co Ci idealne rozwiazanie, dla samej satysfakcji? Bo tak rozumiem slowo tutaj koszerne uzyte, czyli takie by bylo bez skazy. Z drugiej strony mozesz patrzec na slowo koszerne z punktu widzenia wymagan i planu rozwoju aplikacji. Wtedy nasze rozwiazanie nawet w takiej formie jest “koszerne”

          Mam wrazenie ze zakladasz ze jest tam mur ktory wymaga armaty.

          moze i PS ma mozliwosc przemapowania rol na application role, jednak musisz miec wymog ze masz rozne role w roznych aplikacjach… jezeli masz prosty system, to dalej oczywiscie mozesz mapowac to sobie tylko po to by miec PS. tylko po co Ci kolejny klocek do utrzymywania w tym wypadku, tak szczerze? bo ladnie na diagramie wyglada? Bo daje dodatkowe mozliwosci – jednak skoro wiesz ze przez 6 lat sie nic nie zmienilo i przez kolejne 4 tez sie nie zmieni, to dalej masz placic za PS? A co bedzie za 4 lata? jakie starndardy i opcje beda dostepne?

          “nie uwzam ze to jest dobre” – uwazam, ze zmiana wartosci claim niepowinna byc wykonywana przez uyztkownika w jakikolwiek sposob dosc czesto. Bo jak juz na LI bylo powiedziane, cliam opisuja osobe. Jednak jezeli juz cos takiego sie dzieje, to chcialbym by user byl raczej tego swiadomy. I by ponownie fizycznie sie zalogowal do aplikacji.

          W locie tez nie zmieniasz praw dostepu ktore nagle maja wplyw na to co widzisz na stronie. Musisz je jakos odswiezyc – i strone tez. Uwazam, ze dla bezpieczenstwa, taka osoba powinno sie wylogowac przy zmianach praw dostepu by nie nastapil jakis special case, ktory nagle umolziwi osobie zrobiebie czegos czego nie poiwnna moc.

          Wiec wracajac do meritum, jezeli masz wymagania ktore wymagaja zarzadzia uprawnieniami per aplikacja i roznimi poziomami dostepu do tego, robiebie tego w Idsrv jest zle i powinno sie to wyciagnac.

          Jezeli zas nie masz takiego wymagania, masz liste 3-4 prostych przejrzystych role ktore sa odgornie stworzone 10 lat temu i przechodza trzecia juz iteracje systemu (zapomanilem o orginalnym systemie z ktory byl przed tym z ADFS) i dalej spelniaja swoje wymagania i nie ma potrzebny tego zmieniania.

          No chyba, ze dla wlasnej sasysfakcji :)

          to co jest fajne, to cala dyskusja, ze da sie to zrobic inaczej tylko, ze koszt wprowadzenia tego rozwiazania jest niesamowicie duzy przy minimalnym jego zysku w tym konkretnym przykladzie.

          Czyli ogolnie mam rozumiec, gdybys mial taki case u siebie w firmie, to bys postawil PS (wlasny bo ten opensource jest juz archived i przestarzaly)? Niezaleznie od tego jakie bylyby plany na przyszlosc?

          • Mam wrażenie, że odebrałeś mój komentarz jako atak. Zupełnie niepotrzebnie ;)

            O ile jestem zwolennikiem separacji autoryzacji i uwierzytelnienia to zdecydowanie nie jestem też zwolennikiem robienia czegoś tylko dla samej satysfakcji i aby było ładnie/koszernie. Jeśli dobrze rozumiem Twój przypadek to zgadzam się, że wyciąganie tutaj armaty w postaci PS, custom’owego rozwiązania itp. nie ma sensu.

          • @Michał jak brzmie jakby to byl atak to sorki. widac koniec dnia daje mi sie w znaki :) Staralem sie dawac “:)” by nie brzmialo tak jak zabrzmialo :)

            moze po prostu dziwie sie, ze przy odswiezeniu claim wynikla dyskusja o Policy Server (btw w .NET jest Policy Authorization z ktorego korzystamy) :) w zyciu tez bym raczej nie pomyslal o Policy Server przy odswiezaniu wartosci claim. Jakbym zmienial imię i miałbym doświeżyć imię użytkownika na stronie i we wszystkich zakładach to bym to zrobił w ten sposób jaki opisałem.

            Organizacja tak naprwadę nie jest uprawnieniem, jest ID po ktorym się dane filtruje. Czyli pewnie cale podejscie trzeba by bylo zmienic, i stworzyć z organizacji jakieś biznesowy byt który można traktować jak tentant. Ogolnie ciekawa rzecz do rozkminiki ale to dopiero za 4 lata :)

    • by nie bylo. rozumiem Twoj tok myslenia i sie z nim zgadzam. jednak wiem, ze tez sie zgodisz z tym, ze to nie jest must tylko ze tak bedzie clean. I jak nie ma potrzeby to nie ma sensu tego robić.

      Zas cos na ksztal PS jest juz w .NET: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-3.1 i korzystamy z tego przy prawach dostępu do określonych rzeczy w zależności od roli. Nie organizacji, organizacja tutaj ma sie nijak do rol.

  2. W sumie jak już Tomek O. się wypowiedział, to w sumie nie wypada mi nic tutaj dodawać ;-). Dodam tylko, że była kiedyś propozycja standardu opisująca coś takiego jak OAuth2 Token Exchange (https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16#page-3). Definiuje on impersonację i delegację w kontekście OAuth2, ale do tej pory pozostaje tylko draftem.
    Myśleliśmy kiedyś o implementacji tego u siebie, ale w końcu temat został zarzucony jako generujący zbyt dużo problemów – KISS ;-)

    • I tak i nie – zbyt płytko. OAuth autoryzuje Ci dostęp do API a to zbyt mało granularnie jest jak o tym pomyślisz w wielu wypadkach, przykład:
      – Masz dostęd do “read’ w api ale tylko do niektórych danych i API musi sprawdzić, czy query które zadałeś zawiera się w tych danych, do których masz dostęp (real life case)

      • chyba za bardzo wchodzicie to ze zmiana organizacji wiaze sie niewiadomo co.

        zmiana organizacji wiaze sie jedynie z filtrem na dane.

        mozesz to rzobijac na organizacja.read itp itd, jednak jak juz wczesniej pisalem, to jest armata na problem ktorego nie ma a ktory bysmy sobie stworzyli robiac rodzielenie dla tak prostego pojedynczego case.

        wiec jak juz padlo KISS.

ZOSTAW KOMENTARZ

Please enter your comment!
Please enter your name here