Ostatnio musiałem napisać autoryzację z jednym z serwisów w który używa OAuth w wersji 1 i muszę powiedzieć, że tyle przecinków ile się posypało przy tym dawno nie wydobyło się z moich ust.

OAuth2 jest dość przyjemnym protokołem – da się z niego normalnie korzystać, jednak z OAuth1 tak prosto i fajnie już nie jest.

Pierwszy zgrzyt na jaki natrafiłem to tak zwane podpisywanie requestów. Niby wszystko prosto, jeżeli chcemy zrobić request do http://www.flickr.com/services/oauth/request_token (plus query parameters), to powinniśmy dodać do niego sygnaturę. Taką sygnaturę generuje się na podstawie różnego rodzaju podpisów (przy Flickr jest to HMAC-SHA1), i to by nie stanowiło jakoś dużego problemu gdyby nie to, że by móc podpisać request musimy odpowiednio skonstruować string do podpisania.

Przy czym, wielkość liter ma ogromne znaczenie, stąd na przykład przy metodzie GET nasz string musi się rozpoczynać od GET (analogicznie POST), następnie dodajemy znak and (&) i odpowiednio z encodowany URL naszego requestu. Niby wszystko proste, jednak nie możemy w pełni wykorzystać URL Encoding, ze względu na to, iż OAuth encoding różni się od URL Encoding. OAuth wspiera wszystkie normalne literki, plus 4 znaku punkcyjne takie jak _, -, . oraz ~. Dodatkowo, spacja musi koniecznie być zamieniona na %20 itp. itd.

Jak już się z tym uporamy, to musimy dodać kolejną część do naszego stringa, znów rozdzielamy go za pomocą and (&). Nasz query string łączymy w pojedynczy ciąg znaków normalnie rozdzielając kolejne elementy za pomocą& i wykorzystując odpowiednie encodowanie dla parametrów query string. Warto tutaj zwrócić uwagę na to, że parametry muszą być posortowane alfabetycznie po kluczu – jest to bardzo ważne.

Do takiego zapytania musimy dodać nonce – losowo wygenerowany klucz, który następnie wykorzystamy w zapytaniu, także timestamp (w formacie sekund od czasu stworzenia Unix). Parametry niezbędne są przeważnie takie same, jednak czasami się różnią, dla przykładu Flickr wymaga (na końcu postu udostępniłem trzy metody, jedna do nonce, druga do timestamp i trzecia do podpisywania):

  • oauth_callback
  • oauth_consumer_key
  • oauth_nonce
  • oauth_signature_method
  • oauth_timestamp
  • oauth_version – opcjonalne, ale lepiej dodać

Mając już nasz string z query parameters, robimy na nim po raz kolejny OAuth encoding.

Następnie łączymy wszystkie części naszego stringa do podpisu i wykonujemy hashowanie. Kluczem hashowania musi być string, który powstaje za pomocą dwóch złączonych wartości:

Key = oauth_consumer_key&token_secret

Jeżeli nie posiadamy token_secret to go pomijamy ale zostawiamy &.

Tak o to, możemy podpisać nasze zapytanie.

// to co chcemy uzyskac:
http://www.flickr.com/services/oauth/request_token
  ?oauth_callback=http%3A%2F%2Fmy.cystom.callback.url.com
  &oauth_consumer_key=653e7a6ecc1d528c516cc8f92cf98611
  &oauth_nonce=XYZacdfACDF
  &oauth_signature_method=HMAC-SHA1
  &oauth_timestamp=1366888668
  &oauth_version=1.0
  
  
// krok 1: typ metody
GET

// kork 2: encoding URL do requst_token
http%3A%2F%2Fwww.flickr.com%2Fservices%2Foauth%2Frequest_token

// krok 3a: laczenie query string (linie dodane dla czytelnosci normalnie powinna to byc jedna linia
oauth_callback=http%3A%2F%2Fmy.cystom.callback.url.com
&oauth_consumer_key=653e7a6ecc1d528c516cc8f92cf98611
&oauth_nonce=XYZacdfACDF
&oauth_signature_method=HMAC-SHA1
&oauth_timestamp=1366888668
&oauth_version=1.0

// krok 3b: encoding kroku 3a
oauth_callback%3Dhttp%253A%252F%252Fmy.cystom.callback.url.com
%26oauth_consumer_key%3D653e7a6ecc1d528c516cc8f92cf98611
%26oauth_nonce%3DXYZacdfACDF
%26oauth_signature_method%3DHMAC-SHA1
%26oauth_timestamp%3D1366888668
%26oauth_version%3D1.0

// krok 4: laczymy wszystko w calosc

krok1&krok2&krok3b

// krok 5: generujemy klucz:

653e7a6ecc1d528c516cc8f92cf98611&

// krok 6: podpisujemy
// krok 7: wykonujemy request

Mając już podpis tworzymy request w którym oprócz wszystkich wyżej wymienionych parametrów dodajemy jeszcze parametr oauth_signature.

Dopiero teraz możemy wykonać zapytanie, jak wszystko pójdzie dobrze, to otrzymamy odpowiednią odpowiedź. W przeciwnym wypadku musimy się męczyć i zgadywać cośmy źle zrobili.

W zależności od tego co i jak zostało zaimplementowane, możemy uzyskać całkiem porządną odpowiedź – na przykład nie poprawny signature i przykład poprawnego.

W takich wypadkach musimy porównać 1-1 to co my encodujemy i to co wysyłamy i to co otrzymaliśmy – w 99% jest coś nie tak, jak na przykład mała literka zamiast dużej. Albo nasz klucz użyty w podpisywaniu jest niepoprawny, jednak tego się już tak łatwo nie dowiemy.

Niby wszystko fajnie, robimy ten pierwszy requst otrzymujemy odpowiedź i chcemy wykonać kolejny request… co musimy zrobić? I tak o to powstał drugi zgrzyt: Zacząć od początku, wygenerować podpis, dobrze to podpisać itp. itd.

Zaczyna to być potwornie żmudne i wkurzające, w szczególności, kiedy dopiero przy 5-6 request okazuje się, że coś jest nie tak i czegoś gdzieś nie mamy :/

Na szczęście istnieje biblioteka, która nam w tym pomoże (mowa o bibliotece która umożliwi wykonywanie odpowiednich requestów z wykorzystaniem OAuth, nie authentication provider!), ale o tym następnym razem :)

Czy ktoś z was w ogóle korzysta z OAuth? Bawiliście się z OAuth1? Może macie jakieś godne polecenia biblioteki, które zwrócą wam AccessToken i user information po zalogowaniu? Ja osobiście po wszystkich przejściach zacząłem korzystać z World Domination – bardzo przyjemna i prosta w obsłudze biblioteka umożliwiająca logowanie do kilku providerów.

Poniżej obiecany kod:

public static string GenerateNonce()
{
    return Guid.NewGuid().ToString("n");
}

public static long GetUnixTime()
{
    return ApplicationTime.Current.ToUnixTime();
}

public static long ToUnixTime(this DateTime? @this)
{
    if (@this.HasValue == false)
    {
        return 0;
    }

    return (long)(@this.Value - new DateTime(1970, 1, 1)).TotalSeconds;
}

public static string GetHmacSha1(this string @this, string key)
{
    var keyBytes = Encoding.UTF8.GetBytes(key);
    var sha1 = new HMACSHA1(keyBytes);
    var signatureBytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(@this));
    return Convert.ToBase64String(signatureBytes);
}