ASP .NET Core (.NET 5) - walidacja tokenów na podstawie własnych obiektów oraz warunków

0

Witam.
Potrzebuje nakierowania czy jest opcja walidacji tokenów na podstawie własnego "widzimisie". Staram się stworzyć "panel" www do Comarch Optima korzystając z ich bibliotek. Problem polega na tym, że token jest generowany w momencie kiedy ktoś poprawnie zaloguje się do Optimy (za pomocą ich bibliotek oczywiście, abstrakcyjnie).

Jeżeli podano poprawne dane to obiekt ILogin wrzucam do ConcurrentDictionary<int, ILogin> i z tego słownika później jest pobierany jego ILogin na podstawie ID użytkownika. Token jest mi potrzebny do wyciągania danych, tę kwestie robię Dapperem ale dodawanie dokumentów, towarów, kontrahentów robię już bibliotekami Comarchu i tutaj robi się cyrk czasem. Szczególnie jak uruchomię aplikację ponownie to token mam w localStorage, ale ILogin już nie istnieje w słowniku więc powinienem delikwenta wylogować i kazać mu się zalogować na nowo. Ogólnie to chciałbym w kilku miejscach weryfikować czy istnieje w słowniku ILogin tego konkretnego użytkownika i wydaje mi się, że tokenem najlepiej.

Jest jeszcze opcja żeby zrobić ValidationController i co jakiś czas odpytywać czy ILogin istnieje na podstawie ID użytkownika ale mam przeczucie, że idzie to zrobić prościej.

0

Jak nie masz loginu w słowniku to zwracasz 401 (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) i na froncie przekierowujesz na stronę logowania. W słoniku powinieneś trzymać mapowanie tokena na Ilogin, a nie ID - bedzie łatwiej sprawdzać, czy użytkownik jest zalogowany - przekazujesz token z frontu w headerze i w metodzie sprawdzasz, czy w słoniku jest wpis. Jak jest to ok i robisz co ma się wydarzyć jak nie to każesz się mu zalogować.
Zamierzasz używać tych bibliotek COMowych Optimy w .Net core? Masz juz to opracowane? Też pracuje nad API do optimy i zrezygnowałem z Cora, bo tam jakieś szpagaty trzeba robić z tymi obiektami COM. W zwykłym .net lepiej to działa.

0
UglyMan napisał(a):

W słoniku powinieneś trzymać mapowanie tokena na Ilogin, a nie ID - bedzie łatwiej sprawdzać, czy użytkownik jest zalogowany

Hmm... Tylko w .NET Core autoryzacja odbywa się automatycznie za pomocą atrybutu [Authorize]. Nigdy nie potrzebowałem tokenu w "czysty" sposób, aby coś zweryfikować.

UglyMan napisał(a):

Zamierzasz używać tych bibliotek COMowych Optimy w .Net core? Masz juz to opracowane? Też pracuje nad API do optimy i zrezygnowałem z Cora, bo tam jakieś szpagaty trzeba robić z tymi obiektami COM. W zwykłym .net lepiej to działa.

Tak, obiektami COM to robię. Na pewno nie możesz używać "nowego" logowania za pomocą Common.dll oraz Common.Logic.dll, ponieważ to jest stricte .NET Framework i z tym sobie nie poradzi Core. Owszem, trochę gimnastyki z tym jest ale doszedłem do takiego etapu, że nawet drukuje dokumenty (GenRap i Crystal). Poświęciłem na to sporo czasu ale szefy twierdzą, że warto, więc klepie dalej. Jak na razie wszystko idzie (bardzo wolno) w dobrym kierunku. Problemem jest liczenie tego wszystkiego po stronie SPA (netto, brutto, rabaty). Obiekty COM wrzucą dokument poprawnie ale będzie to widoczne dopiero po zapisie, a to może mylić użytkownika.

UglyMan napisał(a):

W zwykłym .net lepiej to działa.

Jakoś oswoiłem się z Core i nie chciałem już wracać do Framework. To nie jest świadome programowanie tylko metoda prób i błędów. Optima jest stworzona z myślą o okienkach i nawet jako Windows Service potrafi narobić kłopotów, a Comarch jak zwykle rączki umywa, bo to nie ich problem.

0
AdamWox napisał(a):
UglyMan napisał(a):

W słoniku powinieneś trzymać mapowanie tokena na Ilogin, a nie ID - bedzie łatwiej sprawdzać, czy użytkownik jest zalogowany

Hmm... Tylko w .NET Core autoryzacja odbywa się automatycznie za pomocą atrybutu [Authorize]. Nigdy nie potrzebowałem tokenu w "czysty" sposób, aby coś zweryfikować.

Aktualnie pracuje nad api do Optimy i tak mi wyszło, że lepiej to zrobić po swojemu. Nie znaczy, że jest to jedyne słuszne rozwiązanie i że jest poprawne.

Co do COMów w .net core to uznałem, że szkoda zabawy - jak .net framework przestanie być wspierany to i Optima przestanie istnieć albo zostanie przepisana na coś innego. Mój plan to API w zwykłym .net a front w Blazor i .net Core - zobaczymy co z tego wyjdzie.

W zwykłym .net lepiej to działa.

Jakoś oswoiłem się z Core i nie chciałem już wracać do Framework. To nie jest świadome programowanie tylko metoda prób i błędów. Optima jest stworzona z myślą o okienkach i nawet jako Windows Service potrafi narobić kłopotów, a Comarch jak zwykle rączki umywa, bo to nie ich problem.

To nie jest w interesie Comarch, zwłaszcza że oni teraz tego XT mocno promują.

0

Owszem, identyfikacja po tokenie jest zdecydowanie lepsza. ID może się powtórzyć jeśli wybiorę inną bazę konfiguracyjną. ID użytkownika = ID operatora w bazie konfiguracyjnej.

Mój plan to API w zwykłym .net a front w Blazor i .net Core - zobaczymy co z tego wyjdzie.

No właśnie takie "pośredniki" mi nie pasowały. Chciałem mieć wszystko w jednym miejscu. Akurat w angularze mam większe doświadczenie niż w Blazor ale front w tym przypadku nie ma za dużego znaczenia.

To nie jest w interesie Comarch, zwłaszcza że oni teraz tego XT mocno promują.

No właśnie XT był mocnym argumentem przeciw temu całemu przedsięwzięciu. Bo skoro jest już coś, to po co tracić czas i robić to samo.

0

Ja tylko dodam, że XT nigdy nie wyprze Optimy. XT jest dla malutkich firm (w okolicach np. do 200 dokumentów - przy większej liczbie robi się to męczące), które chcą same sobie prowadzić księgowość w przeglądarce internetowej.
Optima jest nieporównywalnie wygodniejsza i jeśli firma ma już więcej dokumentów lub jest to biuro rachunkowe to jedyna słuszna opcja to właśnie Optima.
Dodatkowo XT jest tylko do ewidencji uproszczonej (KPiR), natomiast w Optimie jest również m.in. pełna księgowość a pozostałe moduły są znacznie bardziej dopracowane.

A tak z ciekawości zapytam jakiego typu aplikacje połączone z Optimą piszecie?
Jakby co to ja jestem księgowym i właśnie pracuję na Optimie więc coś o niej wiem ;)

I też przymierzam się do napisania własnej apki pod Optimę w blazorze :P ale ja głównie będę wyciągał dane bezpośrednio z bazy niż je tam dodawał (w razie takiej potrzeby dane w postaci XML-a będą mogły być importowane bezpośrednio przez użytkownika) - dlatego u mnie będzie troszkę prostszy model :).

1

Owszem, masz rację ale obawiam się, że robienie (www, mobile) pełnej funkcjonalności pod Optimę jest zbyt czasochłonne. Plan na tę chwilę jest taki, że robimy podstawy - towary, kontrahenci, dokumenty, ewentualnie płatności i tyle. Ja sobie włosy z głowy wyrywam jak widzę pole "licz dokument od", który ma opcję netto lub brutto, później rabaty, cennik indywidualny... Księgowość zdecydowanie odpada, chyba, że ktoś faktycznie się do projektu dołączy i dopisze to co potrafi najlepiej.

U mnie raczej to są małe programy, na zlecenie dla firm, których jesteśmy opiekunem w Comarch:

  1. Łączniki pomiędzy Optima a Allegro - zapisywanie RO, tworzenie WZ, tworzenie FA/PA z weryfikacją stanów magazynowych, atrybuty statusu magazynowego i tracking
  2. Integracja zamówień z systemem Smyk - generowanie RO, awizacja w postaci XML, pliki COMMAVAL ze stanami magazynowymi
  3. Allegro Stock - kolejny łącznik Optima -> Allegro, stany magazynowe towarów złożonych na podstawie stanów magazynowych składników, kończenie i wznawianie aukcji, zmiana cen
  4. OptimoTrade - nasz własny Optima Detal ale to nie jest takie popularne
  5. OptimoPostman - system do powiadomień o statusach, dokumentach, płatnościach, e-faktura drogą mailową

PS.
Zrobię może dzisiaj jakiś filmik z tego co już mam na www pod Optimę. Jak mi tylko klucz serwisowy oddadzą 😂

0
Kofcio napisał(a):

Ja tylko dodam, że XT nigdy nie wyprze Optimy. XT jest dla malutkich firm (w okolicach np. do 200 dokumentów - przy większej liczbie robi się to męczące), które chcą same sobie prowadzić księgowość w przeglądarce internetowej.

Optima jest nieporównywalnie wygodniejsza i jeśli firma ma już więcej dokumentów lub jest to biuro rachunkowe to jedyna słuszna opcja to właśnie Optima.
Dodatkowo XT jest tylko do ewidencji uproszczonej (KPiR), natomiast w Optimie jest również m.in. pełna księgowość a pozostałe moduły są znacznie bardziej dopracowane.

No ale XT to system rozwojowy. Nie planują robić klienta webowego do Optimy, bo ma ja zastąpić kiedyś XT. Więc raczej celem XT jest wyparcie otimy - pytanie tylko ile to zajmie czasu. Jak np przestaną wspierać Optime i implementować tam zmiany prawne to szybko ludzie przejdą na XT (jak będą dostępne migratory) albo pójdą w coś zupełnie innego. Swoja drogą w zeszłym roku zakończyło się wpasrcie Klasyki (tej takiej aplikacji dosowej), czyli pewnie Optime jeszcze potrzymają z 10 lat.

A tak z ciekawości zapytam jakiego typu aplikacje połączone z Optimą piszecie?
Jakby co to ja jestem księgowym i właśnie pracuję na Optimie więc coś o niej wiem ;)

I też przymierzam się do napisania własnej apki pod Optimę w blazorze :P ale ja głównie będę wyciągał dane bezpośrednio z bazy niż je tam dodawał (w razie takiej potrzeby dane w postaci XML-a będą mogły być importowane bezpośrednio przez użytkownika) - dlatego u mnie będzie troszkę prostszy model :).

Ja na razie piszę rest api (funkcjonalność nie określona) dalej będę robił w razie potrzeby.

3

Ja chyba nie zrozumiałem, czy to o to chodzi, ale jeżeli chcesz mieć własny middleware do sprawdzania podczas [Authorize], to oczywiście możesz: robisz sobie własną klasę dziedziczącą po AuthenticationHandler, w niej robisz sobie własną metodę AuthenticateAsync i w niej decydujesz co i jak, także na podstawie "surowych" danych z żądania oraz ewentualnie tworzysz odpowiednie claims.

Ja mam zrobiony własny uwierzytelniacz który sprawdza, czy w nagłówkach żądania jest nagłówek o odpowiedniej nazwie i wartości - przejrzyj sobie https://github.com/ktos/Ktos.AspNetCore.Authentication.ApiKeyHeader/blob/master/src/ApiKeyHeaderAuthenticationHandler.cs

0

Zgadza się i próbowałem to właśnie tak ogarnąć, ponieważ to byłoby najlepsze rozwiązanie. Problem zaczyna się w momencie kiedy trzeba do tego własnego [Authorize] wrzucić token, albo jakiś inny parametr, który by zidentyfikował odpowiedni ILogin. Ten parametr wpada dopiero na endpointcie. Jak go przekazać?

PS.
Czy ja dobrze zrozumiałem twój kod? Czy ty przypadkiem nie zrobiłeś własnej autentykacji ("logowania")? Ja potrzebuje wrócić użytkownika do logowania jeśli nie ma ILogin lub z innych przyczyn sesja w Optimie wygasła. Własny [Authorize] powinien dziedziczyć po AuthorizeAttribute oraz IAuthorizationFilter sugerując się tym artykułem.

0

A czy ty też nie musisz zrobić własnego uwierzytelniania ("logowania"?). Bo ja zrozumiałem to tak: logujesz się do Optimy i dostajesz token i ten token masz teraz sprawdzać w swojej aplikacji.

0

We własnym uwierzytelnianiu loguje się do Optimy i jeśli wszystko przejdzie poprawnie to token dopisuje do ConcurrentDictonary<string, ILogin> wraz z obiektem ILogin i zwracam token na front. Następnie odpytując dany endpoint chciałbym sprawdzić czy ConcurrentDictionary zawiera ILogin na podstawie przesłanego tokena, jeśli nie to ma pokazać stronę logowania. W moim przypadku [Authorize] powinno sprawdzić czy token istnieje i czy jest poprawny ale dodatkowo w słowniku powinno znaleźć odpowiedni ILogin i sprawdzić czy sesja w samej Optimie nie wygasła.

Logowanie (uwierzytelnianie) mam ogarnięte. Potrzebna mi jest teraz weryfikacja w momencie kiedy np. będzie restart serwera - token istnieje i jest poprawny, ale nie ma obiektu ILogin. Ktoś wyrzuci ręcznie użytkownika z Optimy ("zabierze" licencję) - jest token, jest ILogin ale sesja wygasła.

0

Na tę chwilę udało mi się to rozwiązać w następujący sposób:

Klasa OptimoAuthorizationRequirement - "wymagania". Nie wiem po co. Nie mam w niej żadnej logiki.

    public class OptimoAuthorizationRequirement : IAuthorizationRequirement
    {
        public OptimoAuthorizationRequirement()
        {
        }
    }

Klasa OptimoAuthorizationHandler - sprawdzam czy wymagania zostały spełnione przetrzepując ConcurrentDictionary

    public class OptimoAuthorizationHandler : AuthorizationHandler<OptimoAuthorizationRequirement>
    {
        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OptimoAuthorizationRequirement requirement)
        {
            var httpContext = (DefaultHttpContext)context.Resource;
            string token = await httpContext.GetTokenAsync("access_token");

            if (!string.IsNullOrEmpty(token))
            {
                if (OptimoLogin.logins != null)
                {
                    if (OptimoLogin.logins[token] != null)
                    {
                        ILogin login = OptimoLogin.logins[token];
                        // w tym miejscu dalej czekam na odpowiedź Comarchu jak sprawdzić stan sesji
                        context.Succeed(requirement);
                    }
                    else
                    {
                        context.Fail();
                    }
                }
                else
                {
                    context.Fail();
                }
            }
            else
            {
                context.Fail();
            }
        }
    }

Startup.cs

            services.AddAuthorization(options =>
            {
                options.AddPolicy("OptimoAuthorization", policy =>
                    policy.Requirements.Add(new OptimoAuthorizationRequirement()));
            });

            services.AddSingleton<IAuthorizationHandler, OptimoAuthorizationHandler>();

UŻYCIE

[Authorize(Policy = "OptimoAuthorization")]

Co najciekawsze w tym wszystkim? Domyślna walidacja tokenów w dalszym ciągu działa. Wydawało mi się, że jak robisz swój handler do autoryzacji to to nadpisuje domyślne funkcje. Czy to dlatego, że ja tylko dodałem własny IAuthorizationRequirement?

2

@AdamWox: Domyślna walidacja będzie działać. Trzeba odróżnić uwierzytelnianie od autoryzacji. Pierwsze weryfikuje czy dany użytkownik jest tym, kim jest, czy token nie wygasł itd.
Autoryzacja, którą tutaj wykorzystałeś poprzez własne policy weryfikuje czy użytkownik ma uprawnienia do danego zasobu, operacji, czy posiada daną rolę itp.
W twoim przypadku możesz przejść na własny schemat uwierzytelniania. Przykładowa implementacja:

Opcje schematu:

public class OptimaAuthenticationOptions : AuthenticationSchemeOptions
{
    public const string DefaultScheme = "Optima Scheme";
    public string Scheme => DefaultScheme;
    public string AuthenticationType = DefaultScheme;
}
public class OptimaAuthenticationHandler : AuthenticationHandler<OptimaAuthenticationOptions >
{
    //tutaj możesz dodatkowo wstrzelić jakiegoś httpClient, repository cokolwiek co pomoże zweryfikować stan sesji po stronie Optimy
    public OptimaAuthenticationHandler(IOptionsMonitor<OptimaAuthenticationOptions > options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }
    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
         // walidacja tokenu
        // weryfikacja ILogin ...
    }
}

Rejestracja schematu uwierzytelniania dla całego systemu:

public static AuthenticationBuilder AddOptimaSupport(this AuthenticationBuilder authenticationBuilder, Action<OptimaAuthenticationOptions > options)
{
    return authenticationBuilder.AddScheme<OptimaAuthenticationOptions , OptimaAuthenticationHandler>(OptimaAuthenticationOptions .DefaultScheme, options);
}

services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = OptimaAuthenticationOptions .DefaultScheme;
        options.DefaultChallengeScheme = OptimaAuthenticationOptions .DefaultScheme;
    })
    .AddOptimaSupport();

1 użytkowników online, w tym zalogowanych: 0, gości: 1