Appearance
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)
OAUTH2 SPECS (RFC 6749)
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
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
- Asiakas painaa Sign Up with Google tai Sign In with Google-nappia verkkokauppasi sivulla
- 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.
- Asiakas kirjautuu sisään Google-tililleen
- Asiakas ohjataan takaisin verkkokauppaan mukanaan ns. Authorization Grant (jwt token)
- Verkkokauppasi lähettää varsinaiselle verkkokaupan taustapalvelulle 4. kohdassa saadun Authorization Grantin autorisointia varten
- Verkkokauppasi taustapalvelussa tarkistetaan Googlelta, että Authorization Grant on vielä validi
- 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.
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
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
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.