Skip to content

AUTENTIKAATIO JA AUTORISOINTI

Autentikaatio tarkoittaa henkilöllisyyden todistamista web-palvelulle. Tavallisesti tämä tehdään kirjautumalla sisään järjestelmään esimerkiksi käyttäjätunnuksen ja salasanan avulla. Käyttäjätunnuksen ja salasanan lisäksi sisäänkirjautumisen yhteydessä voidaan myös käyttää jotakin toista henkilöllisyyden varmistavaa tekijää tai laitetta.

Autorisointi taas tarkoittaa toimenpidettä, jossa käyttäjälle annetaan pääsy rajoitettuihin resursseihin johonkin käyttäjän ominaisuuteen vedoten. Autentikointi tapahtuu aina ennen autorisointia, koska ensin pitää varmistaa, että käyttäjä on kirjautunut sisään ja sitten vasta tarkastella, onko käyttäjällä oikeutta johonkin resurssiin.

Jos käytetään esimerkkinä blogisoftaa, autorisoinnilla voidaan rajoittaa postauksien näkyvyyttä ja hallintaoikeuksia käyttäjille.

  • Sisäänkirjautunut käyttäjä voi nähdä kaikki postaukset ja kommentoida kaikkia postauksia
  • Mutta ainoastaan postauksen tehnyt käyttäjä (postauksen 'omistaja') voi poistaa sen tai muokata sitä
    • poikkeuksena pääkäyttäjät (käyttäjät, joiden ryhmä on 'admin'), jotka voivat poistaa ja muokata kaikkia postauksia olivat he niiden omistajia tai eivät.

OAUTH2 (Open Authorization)

Lyhyesti sanottuna OAUTH2 mahdollistaa web-palveluun X tunnistautumisen toisen palveluntarjoajan Y kautta niin, että sinun (käyttäjä) ei tarvitse tehdä tunnusta ja salasanaa suoraan palveluun X.

Sinä palvelun X kehittäjänä olet kuin työnantaja, jolle hakee töihin uusi, entuudestaan tuntematon työntekijä (palvelusi käyttäjä). Koska et tunne käyttäjää, et aluksi halua ottaa häntä töihin, mutta sitten huomaatkin, että suosittelijana hänellä on vanha hyvä kaverisi (toinen palveluntarjoaja, esim. Google). Päätät kysyä kaveriltasi suosituksia, koska voit luottaa häneen. Jos kaverisi suosittelee uutta työntekijää voit ottaa hänet töihin.

Katsotaan tätä esimerkkien kautta.

KIRJAUDUTAAN VERKKOKAUPPAAN X GOOGLE-TUNNUKSELLA

auth
Olet varmaan nähnyt yo. napin useissa eri palveluissa

Kuvitellaan, että teet verkkokauppasovellusta, johon asiakkaat voivat kirjautua sisään käyttäen käyttäjätunnusta ja salasanaa, mutta et halua tallentaa tietoturvasyistä salasanoja verkkokaupan omaan tietokantaan. Päätät käyttää Google-kirjautumista, jolloin asiakkaat voivat kirjautua omilla Google-tunnuksillaan sinun palveluusi ilman, että sinun tarvitsee huolehtia salasanoista.

SISÄÄNKIRJAUTUMISEN KULKU

  1. Asiakas painaa Sign Up with Google tai Sign In with Google-nappia verkkokauppasi sivulla
  2. Jos asiakas ei ole kirjautunut Google-tunnuksillaan valmiiksi sisään selaimessa, jota hän käyttää, hänet ohjataan Googlen kirjautumissivulle. Jos asiakas on jo valmiiksi kirjautunut, mennään suoraan 4. kohtaan.
  3. Asiakas kirjautuu sisään Google-tililleen
  4. Asiakas ohjataan takaisin verkkokauppaan mukanaan ns. Authorization Grant (jwt token)
  5. Verkkokauppasi lähettää varsinaiselle verkkokaupan taustapalvelulle 4. kohdassa saadun Authorization Grantin autorisointia varten
  6. Verkkokauppasi taustapalvelussa tarkistetaan Googlelta, että Authorization Grant on vielä validi
  7. Jos Authorization Grant on kunnossa, voidaan luoda varsinainen Access JWT Token

Yllä kuvatussa ohjelman kulussa kohdat 3 ja 4 ovat ratkaisevia. Koska käyttäjä ohjataan Googlen-sivulle, salasana ei koskaan edes käy verkkokauppasi sivulla. Onnistuneesta kirjautumisesta käyttäjälle palautetaan Authorization Grant, jonka avulla saa Googlelta varsinaisen lopullisen Access Tokenin. Authorization Grantista ei voi päätellä salasanaa mitenkään.

Jos taas vastaavasti hakkeri saa kaapattua Googlelta tulleen Access Tokenin sinun verkkokauppasi sivuilta, hakkeri ei silti pääse sisään Googlen palveluihin samaisella tokenilla.

auth

JWT (JsonWebToken)

JWT on autentikointi- ja autorisointimekanismi web-palveluihin. Sen avulla pystytään siirtämään turvallisesti tietoja eri web-palvelujen välillä JSON-objekteina. JWT:hen tallennettuun tietoon ja sen eheyteen voi luottaa, koska se on digitaalisesti allekirjoitettu. JWT-tunnisteet voidaan allekirjoittaa joko symmetrisesti salaisella tiedolla (merkkijono, joka on vain ohjelmistossa ja sen haltijan tiedossa, eikä sitä koskaan viedä ohjelmiston ulkuopuolelle), tai assymetrisesti julkisella ja yksityisellä avaimella. Symmetrisesti allekirjoitettu tunniste sekä luodaan että avataan samalla salaisella merkkijonolla. Asymmetrisesti allekirjoitettu luodaan yksityisellä avaimella ja sen voi avata julkisella avaimella.

JWT-tunnisteet voidaan myös salata, mutta me keskitymme tässä vain salamaattomiin tunnisteisiin.

HUOM

Huomaa, että allekirjoitus ei ole sama kuin salaus. Allekirjoituksen tarkoitus on vain taata, että tunnisteisiin tallennetut tiedot ovat eheitä, eli niitä ei ole pystytty 'käpälöimään', mutta se ei tarkoita sitä, ettei sitä pystyisi kukaan muu taho lukemaan.

Koska asymmetrisesti allekirjoitetun tunnisteen luonnissa ja avauksessa käytetään eri avaimia (luonnissa yksityistä ja lukemisessa julkista), tämä on turvallisempi vaihtoehto.

ESIMERKKI

auth
Arkkitehtuurikuva verkkokaupan X hajautetusta mikroserveriarkkitehtuurista.

  • Autentikaatiopalvelu: Tässä mikroservisessä on käyttäjähallinta ja siellä luodaan JWT-tunnisteet

  • Ostoskoripalvelu: Tässä mikroservisessä hallinnoidaan asiakkaiden ostoskoreja

  • Katalogipalvelu: Tässä mikroservisessä on logiikka, jolla selaillaan tuotteita

  • Asiakkuuspalvelu: Tässä mikroservisessä hallinnoidaan verkkokaupan asiakkuuksia

A) Symmetrinen allekirjoitus

Jos tämä arkkitehtuuri toteutetaan symmetrisesti allekirjoietulla JWT-tunnistella, pitää sama salainen merkkijono jakaa kaikille mikroserviseille. Tämä nostaa todennäköisyyttä tietomurrolle, jossa hyökkääjä pystyy varastamaan allekirjoitusmerkkijonon ja esiintyy autentikaatiopalveluna pystyen näin varastamaan kaikki käyttäjätiedot

B) Asymmetrinen allekirjoitus

Jos taas tämä arkkitehtuuri toteutetaan asymmetrisesti allekirjoitetulla JWT-tunnistella, luodaan yksityinen ja julkinen avainpari. Yksityinen avain ei koskaan poistu autentikaatiopalvelusta ja sillä luodaan JWT-tunnisteet.

Sen sijaan julkinen avain jaetaan kaikkiin muihin palveluihin. Julkisella avaimella kaikki muut mikroserviset pystyvät tunnistamaan käyttäjät, koska julkisella avaimella tunnisteen voi lukea. Mutta kukaan ei pysty esiintyymään väärin perustein autentikaatiopalveluna, koska JWT:t luodaan ainoastaan yksityisillä avaimilla, ei julkisilla.

JWT:n rakenne

JWT koostuu kolmesta eri ostasta, headerista, payloadista ja signaturesta, jotka erotetaan pisteellä toisistaan

Miksi pisteellä?

Koska header ja payload ovat Base64-enkoodattuja merkkijonoja, ne on turvallista erottaa toisitaan pisteellä.

Sen jälkeen tähän kokonaisuus allekirjoitetaan tavasta riippuen salaisella merkkijonolla tai yksityisellä avaimella

Header sisältää yleensä ainakin tunnisteen tyypin esim. (JWT) ja allekirjoitukseen käytetyn algoritmin Muista aina tarkistaa algoritmin oikeellisuus. JWT:n speksi sallii algoritmina none, joka käytännössä tarkoittaa, ettei tunniste allekirjoiteta ollenkaan. Älä koskaan hyväksy tunnistetta, jota ei ole joko salattu tai allekirjoitettu

PAYLOAD

Tämä osio vaihtelee monesti tunnisteen käyttötarkoituksen mukaan. JWT:n RFC-speksin mukaan on olemassa muutama ennaltarekisteröity tietue, joita tämä payload-yleensä sisältää

  • iss (issuer) eli myöntäjä on taho tai palvelu, joka on luonut tunnisteen Muista aina tarkistaa tämä
  • sub (subject) eli tunnisteen aihe, monesti tämä voi olla yksilöllinen tunniste käyttäjästä
  • aud (audience) eli se kenelle myöntäjä on tarkoittanut tunnisteen käyttöön Muista aina tarkistaa tämä
  • exp (expiration) eli voimassaoloaika, tai aika, jolloin tunniste vanhenee Älä koskaan hyväksy tunnistetta, joka on vanhentunut tarkastushetkeen menneessä
  • iat (issued at time) myöntöhetki
  • nbf (not before) eli aika josta lähtien tunniste on voimassa hylkää tunnisteet, joiden nbf on tulevaisuudessa
  • jti (token identifier) tämä on yksilöllinen tunniste, jonka moni möyntäjä laittaa mukaan tehdäkseen tunnisteista aina yksilöllisen, vaikka kaikki muut tiedot olisivat samoja

Huom!

Muista, että koska tässä käsitellään allekirjoitettuja tunnisteita (ei siis salattuja), kaikki vastaanottajat pystyvät lukemaan tunnisteiden sisällön.

  • Älä milloinkaan tallenna tunnisteeseen arkaluontoisia henkilötietoja, vaan korvaa ne yksilöllisellä tunnisteella, jota voit sitten käyttää tietokantahaussa (opaque token), kun haet käyttäjän tietoja taustapalvelussa. Näin vältyt henkilötietojen leviämiseltä palvelun ulkopuolelle.
  • Muista myös aina käyttää tarpeeksi pitkää yksityistä avainta allekirjoitukseen, jotta se on vaikeampi murtaa raa´alla laskentateholla,
  • äläkä ikinä käytä symmetrisesti allekirjoitettua tunnistetta, jos mahdollista.

JWT:tä ei kannata tallentaa localstorageen tai sessionstorageen, koska sinne pääsee käsiksi JavaScriptillä.

ALLEKIRJOITUS

Yhdistelmä headerista ja payloadista allekirjoitettuna.

ESIMERKKEJÄ JWT:N JA KEKSIEN KÄYTÖSTÄ

JWT:n LUOMINEN

Eri ohjelmointikielillä on useita eri kirjastoja, joilla JWT-tunnisteita voi luoda ja lukea, käytetään esimerkeissä pyJWT-kirjastoa

SYMMETRINEN ALLEKIRJOITUS

python
# pip install PyJWT[crypto]
import jwt
import uuid

def create(self, sub):
    now = time.time()
    # user.unique_identifier = claims

    access_token = jwt.encode({'sub': sub,
        'iss': 'http://juhaninsiistipythonskripti.com',
        'aud': 'localhost',
        'exp': now + 3600, 'nbf': now - 500, 'iat': now},
        'verysecretsharedkey',
        algorithm="HS256")

access_token = create(str(uuid.uuid4()))
# pip install PyJWT[crypto]
import jwt
import uuid

def create(self, sub):
    now = time.time()
    # user.unique_identifier = claims

    access_token = jwt.encode({'sub': sub,
        'iss': 'http://juhaninsiistipythonskripti.com',
        'aud': 'localhost',
        'exp': now + 3600, 'nbf': now - 500, 'iat': now},
        'verysecretsharedkey',
        algorithm="HS256")

access_token = create(str(uuid.uuid4()))

Yo. esimerkkikoodi luo symmetrisesti allekijroitetun JWT-tunnisteen, jonka subjektina on satunnainen merkkijono. Jos tämän merkkijonon tallentaa tietokantaan käyttäjän tietoihin sisäänkirjautumisen yhteydessä, tällä voi tunnistaa käyttäjän tallentamatta JWT-tunnisteeseen mitään arkaluontoista tietoa itse käyttäjästä

ASYMMETRINEN ALLEKIRJOITUS

bash
# luodaan ensin avainpari

openssl genrsa -out cert/id_rsa 4096
openssl rsa -in cert/id_rsa -pubout -out cert/id_rsa.pub
# luodaan ensin avainpari

openssl genrsa -out cert/id_rsa 4096
openssl rsa -in cert/id_rsa -pubout -out cert/id_rsa.pub
python
# luodaan ensin avainpari

openssl genrsa -out cert/id_rsa 4096
openssl rsa -in cert/id_rsa -pubout -out cert/id_rsa.pub

class AsymmetricToken:
    def __init__(self, private_path, public_path):
        with open(private_path) as f:
            self.private = f.read()

        with open(public_path) as f:
            self.public = f.read()

    def create(self, sub):
        now = time.time()
        # user.unique_identifier = str(uuid.uuid4())

        access_token = jwt.encode({'sub': sub,
                                   'iss': 'http://juhaninsiistipythonskripti.com',
                                   'aud': 'localhost',
                                   'exp': now + 3600, 'nbf': now - 500, 'iat': now},
                                  self.private,
                                  algorithm="RS256")
        return access_token


token = AsymmetricToken('cert/id_rsa', 'cert/id_rsa.pub')
access_token = create(str(uuid.uuid4()))
# luodaan ensin avainpari

openssl genrsa -out cert/id_rsa 4096
openssl rsa -in cert/id_rsa -pubout -out cert/id_rsa.pub

class AsymmetricToken:
    def __init__(self, private_path, public_path):
        with open(private_path) as f:
            self.private = f.read()

        with open(public_path) as f:
            self.public = f.read()

    def create(self, sub):
        now = time.time()
        # user.unique_identifier = str(uuid.uuid4())

        access_token = jwt.encode({'sub': sub,
                                   'iss': 'http://juhaninsiistipythonskripti.com',
                                   'aud': 'localhost',
                                   'exp': now + 3600, 'nbf': now - 500, 'iat': now},
                                  self.private,
                                  algorithm="RS256")
        return access_token


token = AsymmetricToken('cert/id_rsa', 'cert/id_rsa.pub')
access_token = create(str(uuid.uuid4()))

JWT:N LUKEMINEN

Koska nämä tunnisteeton allekirjoitettuja, mutteivat salattuja, kaikki pystyvät lukemaan niiden sisällön, siksi niihin ei koskaan kannata tallentaa arkaluontoisia tietoja. Nyt katsotaan, miten voi varmistua, että JWT, jonka asiakassovellus lähettää palvelimelle on oikeasti validi

SYMMETRINEN ALLEKIRJOITUS

python
def validate(encoded_token):
    claims = jwt.decode(encoded_token, 'verysecretsharedkey', 'HS256', audience='localhost')
    # claimit sisältävät selkokielisen payloadin, eli sen, mitä tokeniin on luontihetkellä tallennettu
    # get_user_by_sub on mikä tahansa toteutus, joka varmistaa, että käyttäjä on oikeasti olemassa
    # se ei ole tässä nyt tärkeää.
    user = get_user_by_sub(claims['sub'])
    return user

TOKEN = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDE3ODBmMi1jZDcxLTRhMDEtYjliOC03NjA3YTU5MmQ3OTEiLCJpc3MiOiJodHRwOi8vanVoYW5pbnNpaXN0aXB5dGhvbnNrcmlwdGkuY29tIiwiYXVkIjoibG9jYWxob3N0IiwiZXhwIjoxNjkzMTUxMjYzLjc0NzA2NywibmJmIjoxNjkzMTQ3MTYzLjc0NzA2NywiaWF0IjoxNjkzMTQ3NjYzLjc0NzA2N30.GDvKXWK26gG5v5bKXDunNjSS7CKw9wk7pPTVAP_JVoKhjGt4hTs_S33ioTnWGJCCxfOQwUtsEKvt9U9-t3iAyCeKWvnMX93pGOeKxtuurIv5UhWntRtgPeVOws47mnOiC4QGg2FsD_21ayk6OP_yJPjesxPH8-a7Tc1ejAXau4S-tQ-Ej9k_-KYOr4R_O7I_8I-s3VvGMK-8OrZlgqK8gW-taqBTy8jO_t1pC9KHt-hFt6-B58fvs-7ED9zc-f7PYyzUTH3dZPdSSDzSstNDEjYh3UR4ss9qq2HaEP2fOcLH5kJ6aRQHREDg-Peg6ii45R9g4QfgZn0Rns9KA3CrVZ1Ftkp9ZM7Tx09kj229--PUtkCpxkiUWLfEgfVc8NukwGlZVW8GEAPW3h0-pXiTyWJKFDgrBS_YMtDIS9TzTAHf_or12kFE18wpkjxra_LpdVZfPrgiZDMQV70pCtpATdj9qDyamkCWW_tWUE3IoW-GCI2KQlMZ8XPTuKZ9KNGDPJUA4NWWlUxuInoxNafUTLynpgAjSL5OmKwccGBttZZSfzeGz3WbPEOTx2VbOkAhC9C8KvF8EZoK9bctzCXrL2VH45qZqL7boC-U3fJF6kpvKfRoBXp_jK_fBp_xSA7zjbYELVHGSCeWMnE0sFUW0oj8tr8MOVEG1Ytcox2xhFM'

logged_in_user = validate(TOKEN)
def validate(encoded_token):
    claims = jwt.decode(encoded_token, 'verysecretsharedkey', 'HS256', audience='localhost')
    # claimit sisältävät selkokielisen payloadin, eli sen, mitä tokeniin on luontihetkellä tallennettu
    # get_user_by_sub on mikä tahansa toteutus, joka varmistaa, että käyttäjä on oikeasti olemassa
    # se ei ole tässä nyt tärkeää.
    user = get_user_by_sub(claims['sub'])
    return user

TOKEN = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDE3ODBmMi1jZDcxLTRhMDEtYjliOC03NjA3YTU5MmQ3OTEiLCJpc3MiOiJodHRwOi8vanVoYW5pbnNpaXN0aXB5dGhvbnNrcmlwdGkuY29tIiwiYXVkIjoibG9jYWxob3N0IiwiZXhwIjoxNjkzMTUxMjYzLjc0NzA2NywibmJmIjoxNjkzMTQ3MTYzLjc0NzA2NywiaWF0IjoxNjkzMTQ3NjYzLjc0NzA2N30.GDvKXWK26gG5v5bKXDunNjSS7CKw9wk7pPTVAP_JVoKhjGt4hTs_S33ioTnWGJCCxfOQwUtsEKvt9U9-t3iAyCeKWvnMX93pGOeKxtuurIv5UhWntRtgPeVOws47mnOiC4QGg2FsD_21ayk6OP_yJPjesxPH8-a7Tc1ejAXau4S-tQ-Ej9k_-KYOr4R_O7I_8I-s3VvGMK-8OrZlgqK8gW-taqBTy8jO_t1pC9KHt-hFt6-B58fvs-7ED9zc-f7PYyzUTH3dZPdSSDzSstNDEjYh3UR4ss9qq2HaEP2fOcLH5kJ6aRQHREDg-Peg6ii45R9g4QfgZn0Rns9KA3CrVZ1Ftkp9ZM7Tx09kj229--PUtkCpxkiUWLfEgfVc8NukwGlZVW8GEAPW3h0-pXiTyWJKFDgrBS_YMtDIS9TzTAHf_or12kFE18wpkjxra_LpdVZfPrgiZDMQV70pCtpATdj9qDyamkCWW_tWUE3IoW-GCI2KQlMZ8XPTuKZ9KNGDPJUA4NWWlUxuInoxNafUTLynpgAjSL5OmKwccGBttZZSfzeGz3WbPEOTx2VbOkAhC9C8KvF8EZoK9bctzCXrL2VH45qZqL7boC-U3fJF6kpvKfRoBXp_jK_fBp_xSA7zjbYELVHGSCeWMnE0sFUW0oj8tr8MOVEG1Ytcox2xhFM'

logged_in_user = validate(TOKEN)

ASYMMETRINEN ALLEKIRJOITUS

python
class AsymmetricToken:
    def __init__(self, private_path, public_path):
        with open(private_path) as f:
            self.private = f.read()

        with open(public_path) as f:
            self.public = f.read()

    def validate(self, encoded_token):
        claims = jwt.decode(encoded_token, self.public, 'RS256', audience='localhost')
        print(claims)
        user = get_user_by_sub(claims['sub'])
        return user
class AsymmetricToken:
    def __init__(self, private_path, public_path):
        with open(private_path) as f:
            self.private = f.read()

        with open(public_path) as f:
            self.public = f.read()

    def validate(self, encoded_token):
        claims = jwt.decode(encoded_token, self.public, 'RS256', audience='localhost')
        print(claims)
        user = get_user_by_sub(claims['sub'])
        return user

SESSION

// todo tähän sessioista jotaki tosi siistiä ja jwtn ja session eroista.

Lapin AMK:n Full Stack opintojaksojen nettisivu.