cyberbezpieczeństwo i programowanie

OneNote API & OAuth2

O

Niniejszy post, zgodnie z obietnicą, opisuje bardzo prostą aplikację webową, która wyświetla użytkownikowi listę jego notatników OneNote. Notatniki są pobierane przy użyciu odpowiedniego zapytania OneNote API, a proces uzyskania dostępu opiera się na protokole OAuth 2.0. Oznacza to, że użytkownik uzyskuje dostęp do swoich notatników dopiero po pomyślnym przejściu procesu uwierzytelnienia za pomocą swojego konta Microsoft.

Aplikacja jest typu proof of concept i ma za zadanie zobrazować mechanizm uwierzytelnienia i autoryzacji OAuth 2 w przypadku usługi OneNote API. Z tego względu zaleca się weryfikację kodu przed jego użyciem w produkcji, szczególnie pod względem bezpieczeństwa. Projekt o nazwie OneNoteNotebooks można znaleźć pod tym adresem:

https://bitbucket.org/mieczyk/onenotenotebooks

Biblioteki

Jak zostało już wspomniane, OneNoteNotebooks jest aplikacją webową, która powstała z użyciem technologii .NET Framework 4.5.1 oraz języka C# 5.0. Do tego wykorzystuje zewnętrzne biblioteki:

NazwaWersjaKrótki opis
NancyFx1.4.3Jest to lekki framework do budowania usług opartych o protokół HTTP. Mogą to być zarówno zwyczajne aplikacje webowe jak i usługi sieciowe typu REST. Na temat tej biblioteki warto powiedzieć trochę więcej, więc niech zrobi to Maciej Aniserowicz w swojej prezentacji Randka z Nancy .
RestSharp105.2.3Prosty klient HTTP, używany w szczególności do pracy z REST API.
Json.NET8.0.3Popularna biblioteka do obsługi formatu JSON (m.in. serializacja i deserializacja).
Dapper1.42Micro-ORM (Object-Relational Mapping).

Opis techniczny

Solucja składa się z dwóch komponentów:

  • Core – zawiera klasy stanowiące logikę aplikacji.
  • Web – stanowi interfejs użytkownika.

Uwierzytelnienie

Zanim użytkownik uzyska dostęp do swoich notatników, musi pomyślnie przejść proces uwierzytelnienia i autoryzacji. Najważniejszym elementem tego procesu jest sam użytkownik, więc omawianie warto zacząć od niego:

public class User
{
    public Guid Id { get; private set; }
    public string UserName { get; set; }
    public string AccessToken { get; set; }
    public DateTime TokenExpirationTime { get; set; }
 
    public User()
    {
        Id = Guid.NewGuid();
    }
 
    public User(TokenInfo tokenInfo) : this()
    {
        UserName = tokenInfo.UserId;
        AccessToken = tokenInfo.AccessToken;
        TokenExpirationTime = DateTime.Now.AddSeconds(tokenInfo.ExpiresIn);
    }
}

Jak widać, użytkownik ma zdefiniowanych kilka właściwości:

  • Id – unikatowy identyfikator typu Guid, generowany automatycznie przy tworzeniu instancji klasy. Identyfikator tego typu jest wymagany przy użyciu modułu Nancy.Authentication.Forms, ale o tym za chwilę.
  • UserName – nazwa użytkownika OneNote, zwracana jako user_id przez usługę konta Microsoft, podczas zapytania o token dostępu. Ta nazwa posłuży później do identyfikacji zalogowanych użytkowników.
  • AccessToken – jak sama nazwa wskazuje, jest to token dostępu przypisany do użytkownika.
  • TokenExpirationTime – czas, w którym wygaśnie ważność tokena dostępu.

Zalogowani użytkownicy, wraz ze swoimi tokenami dostępu, są zapisywani w bazie danych. Baza składa się tylko z jednej tabeli – Users. Do jej utworzenia można wykorzystać załączony skrypt SQL: \helper-scripts\Users.sql.

Forms Authentication

Kiedy miejsce przechowywania pobranych tokenów jest już zdefiniowane, można przejść dalej. Oprócz OAuth 2, wykorzystany został mechanizm Forms Authentication, wspierany przez Nancy (w formie osobnego pakietu NuGet). Aby zrozumieć jak działa, warto przyjrzeć się dokumentacji Nancy, w szczególności sekcji Forms Authentication. Dzięki niemu można wykryć czy użytkownik jest już zalogowany, bez potrzeby odpytywania za każdym razem konta Microsoft.  W celu aktywacji mechanizmu należy umieścić następujący kod we własnej implementacji obiektu Nancy Boostrapper:

public class CustomBootstrapper : DefaultNancyBootstrapper
{
    protected override void RequestStartup(
         TinyIoCContainer container,
         IPipelines pipelines, 
         NancyContext context
    )
    {
        var formsAuthConfig = new FormsAuthenticationConfiguration
        {
            RedirectUrl = "~/account/login",
            UserMapper = container.Resolve<IUserMapper>()
        };
 
        FormsAuthentication.Enable(pipelines, formsAuthConfig);
    }
}

RedirectUrl określa adres URL, pod który powinien trafić niezalogowany użytkownik próbujący dostać się do chronionego zasobu. W UserMapper należy podać własną implementację interfejsu IUserMapper, wymaganą przez Nancy.Authentication.Forms. Interfejs IUserMapper definiuje tylko jedną metodę, która w przypadku omawianej aplikacji wygląda następująco:

public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context)
{
    var user = _service.GetUserFromIdentifier(identifier);
     
    return new UserIdentity(user);
}

i odpowiada za zwrócenie danych użytkownika (IUserIdentity) na podstawie identyfikatora Guid. Implementacja interfejsu IUserIdentity również jest wymagana przez Nancy.Authentication.Forms:

public class UserIdentity : IUserIdentity
{
    public string UserName { get; private set; }
    public IEnumerable<string> Claims { get; private set; }
 
    public UserIdentity(User user)
    {
        UserName = user.UserName;
        Claims = new List<string>();
    }
}

Te dane są później przypisywane do właściwości NancyContext.CurrentUser, na postawie której określa się, czy użytkownik jest aktualnie zalogowany – robią to zdefiniowane metody pomocnicze Nancy, a krótki opis jednej z nich zostanie przedstawiony za chwilę. Warto wspomnieć o tym, dlaczego to klasa User nie implementuje interfejsu IUserIdentity - dzięki temu można było odseparować kod związany z Nancy od logiki aplikacji, a projekt Core nie wymaga referencji do bibliotek Nancy.

Moduł NotebooksModule jest zabezpieczony przed nieautoryzowanym dostępem dzięki metodzie RequiresAuthentication, która sprawdza czy użytkownik został uwierzytelniony w danej sesji. Więcej informacji na ten temat można znaleźć w dokumentacji Nancy, w sekcji Authentication overview. Jeśli użytkownik nie został jeszcze zweryfikowany, metoda zwraca HttpStatusCode.Unautorized i jak wspomniano wcześniej, zostaje przekierowany na stronę logowania.

OAuth 2

Klasa odpowiedzialna za obsługę protokołu OAuth 2 implementuje następujące metody:

public interface IOAuth
{
    string BuildLoginUrl(string responseType = "code");
    string BuildLogoutUrl();
    TokenInfo GetToken(string code);
}

Gdzie:

  • string BuildLoginUrl(string responseType = "code") – buduje i zwraca URL przekierowujący użytkownika do strony logowania odpowiedniej usługi – w tym przypadku jest to konto Microsoft. Adres jest budowany na podstawie dostarczonej konfiguracji, która zostanie opisana za chwilę. Parametr responseType determinuje sposób uzyskania tokena dostępu. Domyślna wartość code wskazuje na tzw. Code Flow.
  • string BuildLogoutUrl() – buduje i zwraca URL, którego wywołanie ma na celu wylogowanie użytkownika z usługi.
  • TokenInfo GetToken(string code) – ta metoda powinna być wykorzystywana wyłącznie w podejściu Code Flow. Jak sama nazwa wskazuje, jej zadaniem jest pobranie tokena dostępu na podstawie uzyskanego wcześniej kodu autoryzacji.

Aplikacja nie zawiera sztywno zakodowanych adresów, ani innych informacji dotyczących uwierzytelniania OAuth 2. Z tego względu klasa implementująca powyższe metody przyjmuje jako parametr konstruktora konfigurację:

public interface IOAuthConfig
{
    string LoginUrl { get; set; }
    string LogoutUrl { get; set; }
    string TokenUrl { get; set; }
    string LoginRedirectUrl { get; set; }
    string LogoutRedirectUrl { get; set; }
    string ClientId { get; set; }
    string ClientSecret { get; set; }
    string Scope { get; set; }
}

Gdzie:

  • LoginUrl – adres URL przekierowujący użytkownika do strony logowania dostawcy tokena.
  • LogoutUrl – adres URL powodujący wylogowanie użytkownika z usługi.
  • TokenUrl – adres URL pod którym można uzyskać token dostępu.
  • LoginRedirectUrl – adres URL, do którego zostanie przekierowany użytkownik po poprawnym zalogowaniu się do usługi. Powinien być to adres omawianej aplikacji – więcej na ten temat można znaleźć w następnej sekcji AccountModule.
  • LogoutRedirectUrl – adres URL, do którego zostanie przekierowany użytkownik po wylogowaniu się z usługi.
  • ClientId – identyfikator klienta nadany aplikacji przez usługę uwierzytelniającą (nadającą token dostępu). Więcej informacji w sekcji
    Rejestracja aplikacji.
  • ClientSecret – sekretny klucz nadany aplikacji przez usługę uwierzytelniającą.
  • Scope – określa jakich uprawnień potrzebuje aplikacja do usługi docelowej (w tym przypadku są to notatniki OneNote).

Wszystkie powyższe ustawienia znajdują się w pliku konfiguracyjnym Web.config, a instancja klasy OAuthConfig jest tworzona przez, wykorzystywany w Nancy, kontener TinyIoC. Należy jednak ręcznie ją zarejestrować w CustomBootstrapper (razem z innymi zależnościami, które nie są automatycznie wykrywane przez Nancy):

protected override void ConfigureApplicationContainer(TinyIoCContainer container)
{
    base.ConfigureApplicationContainer(container);
 
    container.Register<IOAuthConfig>((c, p) => new OAuthConfig(
        loginUrl: ConfigurationManager.AppSettings["OAuth.LoginUrl"],
        logoutUrl: ConfigurationManager.AppSettings["OAuth.LogoutUrl"],
        tokenUrl: ConfigurationManager.AppSettings["OAuth.TokenUrl"],
        loginRedirectUrl: ConfigurationManager.AppSettings["OAuth.LoginRedirectUrl"],
        logoutRedirectUrl: ConfigurationManager.AppSettings["OAuth.LogoutRedirectUrl"],
        clientId: ConfigurationManager.AppSettings["OAuth.ClientId"],
        clientSecret: ConfigurationManager.AppSettings["OAuth.ClientSecret"],
        scope: ConfigurationManager.AppSettings["OAuth.Scope"]
    ));
 
    container.Register<IDatabase>((c, p) => new Database(
        ConfigurationManager.ConnectionStrings["NotebooksDB"].ConnectionString
    ));
 
    container.Register<IUserMapper, UserMapper>();
 
}

Ostatnią rzeczą do omówienia w tej sekcji została klasa TokenInfo, która przechowuje szczegółowe informacje o uzyskanym tokenie dostępu:

public class TokenInfo
{
    [DeserializeAs(Name = "token_type")]
    public string TokenType { get; set; }
 
    [DeserializeAs(Name = "expires_in")]
    public int ExpiresIn { get; set; }
 
    public string Scope { get; set; }
 
    [DeserializeAs(Name = "access_token")]
    public string AccessToken { get; set; }
 
    [DeserializeAs(Name = "refresh_token")]
    public string RefreshToken { get; set; }
 
    [DeserializeAs(Name = "user_id")]
    public string UserId { get; set; }
}

Właściwości powinny być zrozumiałe osobie, która zapoznała się wcześniej z artykułem OneNote API – uwierzytelnienie i autoryzacja. Atrybut DeserializeAsAttribute pochodzi z biblioteki RestSharp i informuje w jaki sposób dane z odpowiedzi HTTP powinny być zmapowane, jeśli nazwy właściwości nie odpowiadają nazwom kluczy JSON.

AccountModule

AccountModule jest modułem Nancy odpowiedzialnym za proces uwierzytelnienia i autoryzacji użytkownika. Definiuje następujące ścieżki:

  • GET /account/login – przekierowuje na stronę logowania usługi uwierzytelniającej (w tym przypadku jest to konto Microsoft).
  • GET /account/logout – przekierowuje do strony powodującej wylogowanie użytkownika z usługi uwierzytelniającej.
  • GET /account/authorize – to właśnie tutaj zostanie przekierowany użytkownik po poprawnym zalogowaniu się do usługi uwierzytelniającej. Ten adres jest podawany jako redirect_url podczas prośby o zalogowanie użytkownika. Tutaj też zostaje przekazany kod uwierzytelniający, jako parametr URL, który zostanie wykorzystany do pobrania tokena dostępu.
  • GET /account/clearup – tutaj zostaje przekierowany użytkownik, który wylogował się z usługi uwierzytelniającej. Ten adres jest podawany jako redirect_url podczas prośby o wylogowanie użytkownika.

Wymagania

Aby uruchomić opisywaną aplikację należy wykonać jeszcze kilka czynności opisanych poniżej.

Hosting IIS

Ponieważ protokół OAuth 2 wymaga, żeby usługa uwierzytelniająca mogła się komunikować z aplikacją, która chce uzyskać dostęp do chronionych zasobów, należy przygotować odpowiednie punkty dostępu (ang. endpoints). Niestety, na chwilę obecną nie można zmusić usługi konta Microsoft, żeby przesyłała informację na adres lokalny localhost, dlatego aplikację należy hostować przez serwer IIS i nadać jej odpowiednią nazwę hosta.

Zakładając, że system ma już zainstalowane i uruchomione Internetowe Usługi Informacyjne (IIS), uruchamiamy menadżera IIS – można to zrobić przez naciśnięcie kombinacji klawiszy WIN + R i wpisanie komendy inetmgr.

W menadżerze dodajemy nową witrynę podając następujące ustawienia:

  • Nazwa witryny – może być dowolna.
  • Pula aplikacja – tutaj została podana pula aplikacji o nazwie mieczyk, która ma ustawioną tożsamość (ang. identity) na LocalSystem. Dzięki temu nie musimy się martwić o nadawanie uprawnień dla tej puli. Należy jednak pamiętać, że nie jest to najlepszy pomysł ze względów bezpieczeństwa, dlatego w produkcyjnych aplikacjach należy unikać takiego podejścia.
  • Ścieżka fizyczna – Bezwzględna ścieżka do aplikacji webowej.
  • Port – ustawiony na 8080. Nie jest to wymagane, ale dzięki temu możemy uniknąć konfliktów z innymi aplikacjami hostowanymi na porcie 80.
  • Nazwa hosta – jest to wartość, która będzie potrzeba przy rejestracji aplikacji na koncie deweloperskim Microsoft.

Istotną sprawą jest również zmapowanie podanej nazwy hosta (notebooks.pl) na adres lokalny (127.0.0.1). Można to zrobić dodając odpowiedni wpis do pliku %WINDIR%\system32\drivers\etc\hosts. Do edycji tego pliku wymagane są uprawnienia administratora. Należy również pamiętać o ustawieniu hostingu IIS w opcjach projektu Web.

Rejestracja aplikacji

Kiedy aplikacja działa na lokalnym serwerze IIS i ma przypisaną nazwę hosta, można przystąpić do rejestracji aplikacji pod adresem: https://account.live.com/developers/applications. Kiedy utworzymy nową aplikację o nazwie OneNoteNotebooks, przechodzimy do jej ustawień.

W zakładce Informacje podstawowe możemy zostawić wszystko bez zmian. Ważniejsza jest zakładka Ustawienia interfejsu API. Pierwsze ustawienie (Aplikacja kliencka dla urządzeń przenośnych lub komputerów ustawione na Nie) wskazuje, że jest to aplikacja webowa. W ustawieniu Adresy URL przekierowań mamy podane dwa adresy należące do naszej aplikacji. Pierwszy wskazuje redirect_url ustawiany podczas zapytania o token. Drugi również wskazuje na redirect_url, ale podany podczas prośby o wylogowanie użytkownika. Tylko zdefiniowane tutaj adresy mogą być przekazywane w parametrze redirect_url. Wszystkie inne adresy spowodują zgłoszenie błędu przez usługę konta Microsoft.

Po wejściu do zakładki Ustawienia aplikacji, można podejrzeć identyfikator klienta (Client ID) oraz tajny klucz klienta (Client Secret).

Konfiguracja

Jak zostało wspomniane wcześniej, ważniejsze ustawienia aplikacji są przechowywane w pliku Web.config. W repozytorium znajduje się przykładowy plik Web.default.config będący tylko szablonem, który należy uzupełnić. Następnie nazwa pliku powinna zostać zmieniona na Web.config. Dane, które należy uzupełnić to:

  • <add key="OAuth.LoginRedirectUrl" value="LOGIN_REDIRECT_URL" />
  • <add key="OAuth.LogoutRedirectUrl" value="LOGOUT_REDIRECT_URL"/>
  • <add key="OAuth.ClientId" value="CLIENT_ID" />
  • <add key="OAuth.ClientSecret" value="CLIENT_SECRET" />
  • <add name="DATABASE_NAME" connectionString="CONNECTION_STRING" />

O autorze

Łukasz Mieczkowski

Programista, który zainteresował się cyberbezpieczeństwem.

Dodaj komentarz

cyberbezpieczeństwo i programowanie

Łukasz Mieczkowski

Programista, który zainteresował się cyberbezpieczeństwem.

Kontakt

Zapraszam do kontaktu za pośrednictwem mediów społecznościowych.