diff --git a/asetukset.edn b/asetukset.edn index 72acedad6a..fe95b6774a 100644 --- a/asetukset.edn +++ b/asetukset.edn @@ -18,6 +18,18 @@ "oam_groups" "Jarjestelmavastaava" "oam_organization" "Liikennevirasto"}} + ;; + ;; Käytetään authentikointiin kun käyttäjä tulee Harjaan + ;; + :todennus-varmistus {;; x-iam-data tokenin public key (käyttäjäroolit) + ;; x-iam-accesstoken avain tulee payloadin mukana, sille ei tarvitse muuttujaa erikseen + :public-key-url #=(eval (harja.tyokalut.env/env "COGNITO_IAMDATA_KEY" nil)) + + ;; Onko Cognito todennuksen token authentikointi päällä? + ;; Voi ongelma tilanteessa laittaa false, jos esim. käyttäjät ei pääse Harjaan ilman hyvää syytä + :todennus-varmistus-paalla? #=(eval (harja.tyokalut.env/env "TODENNUS_VARMISTUS_PAALLA" false))} + + :tietokanta {:palvelin #=(eval (harja.tyokalut.env/env "HARJA_TIETOKANTA_HOST" "localhost")) :tietokanta "harja" :portti #=(eval (harja.tyokalut.env/env "HARJA_TIETOKANTA_PORTTI" 5432)) @@ -48,9 +60,9 @@ :itmf {:url #=(eval (str "tcp://" - (harja.tyokalut.env/env "HARJA_ITMF_BROKER_HOST" "localhost") - ":" - (harja.tyokalut.env/env "HARJA_ITMF_BROKER_PORT" 61626))) + (harja.tyokalut.env/env "HARJA_ITMF_BROKER_HOST" "localhost") + ":" + (harja.tyokalut.env/env "HARJA_ITMF_BROKER_PORT" 61626))) :kayttaja (harja.tyokalut.env/env "HARJA_ITMF_BROKER_KAYTTAJA" "admin") :salasana (harja.tyokalut.env/env "HARJA_ITMF_BROKER_SALASANA" "admin") :tyyppi :activemq @@ -72,7 +84,7 @@ :ulkoinen-sahkoposti {:vastausosoite "harja-ala-vastaa@vayla.fi" ;; :palvelin "solita-service-1.solita.fi" - } + } :api-sahkoposti {:suora? false ;; Lokaalisti voit lähettää sähköpostit suoraa ilman apia :vastausosoite "harja-ala-vastaa@vayla.fi" @@ -89,8 +101,8 @@ :digiroad {:url "https://api.testivaylapilvi.fi/digiroad/externalApi/" ;; Testipalvelimen avain projektin salaisuuksien jakamiseen tarkoitetussa paikassa :api-key #=(eval (some-> - (slurp "../.harja/digiroad-api-key") - (clojure.string/trim-newline)))} + (slurp "../.harja/digiroad-api-key") + (clojure.string/trim-newline)))} :integraatiot {:paivittainen-lokin-puhdistusaika nil} @@ -159,13 +171,13 @@ :yha {:url "https://api.testivaylapilvi.fi/yha/rest/" :api-key #=(slurp "../.harja/yha-api-key")} ;; Testipalvelimen avain projektin jakamassa LastPass-kansiossa. - :velho {:token-url "https://vayla-velho-stg.auth.eu-west-1.amazoncognito.com/oauth2/token" + :velho {:token-url "https://vayla-velho-stg.auth.eu-west-1.amazoncognito.com/oauth2/token" :varuste-api-juuri-url "https://apiv2stgvelho.testivaylapilvi.fi" :varuste-kayttajatunnus "6o9f1j147qvdvo49nufo0fhufp" :varuste-tuonti-suoritusaika nil :varuste-salasana #=(eval (some-> - (slurp "../.harja/velho-varuste-salasana") - (clojure.string/trim-newline))) + (slurp "../.harja/velho-varuste-salasana") + (clojure.string/trim-newline))) :oid-tuonti-suoritusaika nil} ;; LinkSMS eli Labyrintti @@ -202,7 +214,11 @@ :reittitarkistukset :tiedostopesula :replica-db - :sampo} + :sampo + ;; Sampoapi, vai apisampo? + ;; Ei tartte kehitysmoodissa + :sampo-api + :api-sampo} :vaylat {:geometria-url "https://extranet.vayla.fi/inspirepalvelu/avoin/wfs?Request=GetFeature&typename=vaylat&OUTPUTFORMAT=application/json" :paivittainen-tarkistusaika [12 22 00] diff --git a/project.clj b/project.clj index 6eb7db992c..7787adac5b 100644 --- a/project.clj +++ b/project.clj @@ -55,6 +55,10 @@ ;; Pattern match kirjasto [org.clojure/core.match "1.1.0"] + ;; Authentikaatio / kirjautumisen allekirjoituksen varmistus + ;; Täältä tulee java kirjaston kautta jwt signaturen vahvistus, joka tehdään käyttäjän tullessa Harjaan + [buddy/buddy-sign "3.5.351"] + ;; -- Tietokanta: ajuri, kirjastot ja -migraatiot -- ;; Ajuria päivittäessä, muista päivittää myös pom.xml, koska flyway käyttää sitä ajurin versiota diff --git a/src/clj/harja/palvelin/asetukset.clj b/src/clj/harja/palvelin/asetukset.clj index 2af910ab8e..d53c62180c 100644 --- a/src/clj/harja/palvelin/asetukset.clj +++ b/src/clj/harja/palvelin/asetukset.clj @@ -21,7 +21,8 @@ "Harja-palvelinasetuksien skeema" {(s/optional-key :alusta) s/Keyword (s/optional-key :sahke-headerit) {s/Str {s/Str s/Str}} - + (s/optional-key :todennus-varmistus) {:public-key-url s/Str + :todennus-varmistus-paalla? s/Bool} (s/optional-key :clojure-async-thread-poolin-koko) s/Int :http-palvelin {:portti s/Int @@ -51,8 +52,7 @@ (s/optional-key :glog) {(s/optional-key :url) s/Str (s/optional-key :from) s/Str (s/optional-key :to) s/Str - (s/optional-key :q) s/Str - } + (s/optional-key :q) s/Str} (s/optional-key :jira) [s/Str]}} (s/optional-key :email) {:taso s/Keyword diff --git a/src/clj/harja/palvelin/integraatiot/api/ilmoitukset.clj b/src/clj/harja/palvelin/integraatiot/api/ilmoitukset.clj index f0a1a731ef..a33e7b7c22 100644 --- a/src/clj/harja/palvelin/integraatiot/api/ilmoitukset.clj +++ b/src/clj/harja/palvelin/integraatiot/api/ilmoitukset.clj @@ -1,7 +1,6 @@ (ns harja.palvelin.integraatiot.api.ilmoitukset "Tieliikennelmoitusten haku ja ilmoitustoimenpiteiden kirjaus" (:require [com.stuartsierra.component :as component] - [harja.palvelin.komponentit.todennus :as todennus] [org.httpkit.server :refer [with-channel on-close send!]] [clojure.spec.alpha :as s] [clj-time.coerce :as c] diff --git a/src/clj/harja/palvelin/integraatiot/api/tyokalut/kutsukasittely.clj b/src/clj/harja/palvelin/integraatiot/api/tyokalut/kutsukasittely.clj index fa38827988..572b9b4a21 100644 --- a/src/clj/harja/palvelin/integraatiot/api/tyokalut/kutsukasittely.clj +++ b/src/clj/harja/palvelin/integraatiot/api/tyokalut/kutsukasittely.clj @@ -1,7 +1,6 @@ (ns harja.palvelin.integraatiot.api.tyokalut.kutsukasittely "API:n kutsujen käsittely funktiot" (:require [cheshire.core :as cheshire] - [harja.palvelin.komponentit.todennus :as todennus] [taoensso.timbre :as log] [clojure.walk :as walk] [clojure.core.async :refer [ - (assoc req :headers (todennus/prosessoi-kayttaja-headerit (:headers req))) + (assoc req :headers (todennus/prosessoi-kayttaja-headerit (:headers req) kehitysmoodi todennus-varmistus)) (handler)))) (defn wrap-with-common-wrappers "Käärii HTTP-pääkäsittelijän ympärille yleisiä wrappereita." - [handler] - (-> handler - (cookies/wrap-cookies) - (wrap-prosessoi-headerit))) + ([handler] + (wrap-with-common-wrappers handler true nil)) + ([handler kehitysmoodi] + (wrap-with-common-wrappers handler kehitysmoodi nil)) + ([handler kehitysmoodi todennus-varmistus] + (-> handler + (cookies/wrap-cookies) + (wrap-prosessoi-headerit kehitysmoodi todennus-varmistus)))) (defn- jaa-todennettaviin-ja-ei-todennettaviin [kasittelijat] (let [{ei-todennettavat true @@ -365,8 +369,7 @@ kutsu))) (defrecord HttpPalvelin [asetukset kasittelijat sessiottomat-kasittelijat - http-server kehitysmoodi - mittarit] + http-server kehitysmoodi todennus-varmistus mittarit] component/Lifecycle (start [{metriikka :metriikka db :db :as this}] (when metriikka @@ -418,7 +421,10 @@ kehitysmoodi anti-csrf-token-secret-key)) (conj ui-kasittelija))] - (reitita (todennus/todenna-pyynto todennus req) todennettavat-kasittelijat + (reitita (todennus/todenna-pyynto + todennus + req + kehitysmoodi) todennettavat-kasittelijat {:vaadi-oikeustarkistus? true})))) (catch [:virhe :todennusvirhe] _ {:status 403 :body "Todennusvirhe"}) @@ -426,7 +432,9 @@ (finally (metriikka/muuta! mittarit :aktiiviset_pyynnot dec - :pyyntoja_palveltu inc))))) + :pyyntoja_palveltu inc)))) + kehitysmoodi + todennus-varmistus) {:port portti :thread (or (:threads asetukset) 8) @@ -498,9 +506,18 @@ (fn [kasittelijat] (filterv #(not= (:nimi %) nimi) kasittelijat))))) -(defn luo-http-palvelin [asetukset kehitysmoodi] - (->HttpPalvelin asetukset (atom []) (atom []) (atom nil) kehitysmoodi - (metriikka/luo-mittari-ref mittarit-alkuarvo))) +(defn luo-http-palvelin + ([asetukset kehitysmoodi] + (luo-http-palvelin asetukset kehitysmoodi nil)) + ([asetukset kehitysmoodi todennus-varmistus] + (->HttpPalvelin + asetukset + (atom []) + (atom []) + (atom nil) + kehitysmoodi + todennus-varmistus + (metriikka/luo-mittari-ref mittarit-alkuarvo)))) (defn julkaise-reitti ([http nimi reitti] (julkaise-reitti http nimi reitti true)) diff --git a/src/clj/harja/palvelin/komponentit/todennus.clj b/src/clj/harja/palvelin/komponentit/todennus.clj index 2662c7b1cd..d93c989a4f 100644 --- a/src/clj/harja/palvelin/komponentit/todennus.clj +++ b/src/clj/harja/palvelin/komponentit/todennus.clj @@ -2,8 +2,7 @@ "Tämä namespace määrittelee käyttäjäidentiteetin todentamisen. Käyttäjän todentaminen WWW-palvelussa tehdään KOKA ympäristön antamilla header tiedoilla. Tämä komponentti ei tee käyttöoikeustarkistuksia, vaan pelkästään hakee käyttäjälle sallitut käyttöoikeudet ja tarkistaa käyttäjän identiteetin." - (:require [cheshire.core :as cheshire] - [clojure.core.cache :as cache] + (:require [clojure.core.cache :as cache] [clojure.set :as set] [clojure.string :as str] [clojure.data.json :as json] @@ -13,8 +12,9 @@ [oikeudet :as oikeudet]] [harja.kyselyt [kayttajat :as q]] - [slingshot.slingshot :refer [throw+ try+]] - [taoensso.timbre :as log]) + [slingshot.slingshot :refer [throw+]] + [taoensso.timbre :as log] + [harja.palvelin.komponentit.todennus-varmistus :as varmistus]) (:import (org.apache.commons.codec.binary Base64) (org.apache.commons.codec.net BCodec))) @@ -90,7 +90,11 @@ ja urakoitsijan-id funktioita." [urakan-id urakoitsijan-id roolit oam-groups] (let [roolit-ja-linkit (->> (str/split oam-groups #",") - (keep (partial ryhman-rooli-ja-linkki roolit)))] + (keep (partial ryhman-rooli-ja-linkki roolit))) + ;; Uudelleenohjaa käyttäjä jos authentikointi epäonnistuu + roolit-ja-linkit (if (= oam-groups "failed") + [[{:nimi "failed" :kuvaus "Authentikointi epäonnistui." :osapuoli nil :linkki nil} nil]] + roolit-ja-linkit)] {:roolit (yleisroolit roolit-ja-linkit) :urakkaroolit (urakkaroolit urakan-id roolit-ja-linkit) :organisaatioroolit (organisaatioroolit urakoitsijan-id roolit-ja-linkit)})) @@ -112,32 +116,33 @@ (str/join ","))) (defn- pura-cognito-headerit - "Purkaa AWS Cognitolta palautuneet relevantit headerit ja hakee niistä OAM-tiedot. - Tiedot mapataan vanhan mallisiksi OAM_-headereiksi" - [headerit] + "Purkaa AWS Cognitolta palautuneet headerit ja hakee niistä OAM-tiedot. + Tiedot mapataan vanhan mallisiksi OAM_-headereiksi + JWT Signaturen vahvistukset suoritetaan samalla, jonka epäonnistuessa authentikointi ei etene" + [headerit kehitysmoodi? {:keys [public-key-url todennus-varmistus-paalla?]}] + (let [;; Sisältää mm. Cogniton user poolin url:n ja app client id:n, kertoo koska token on annettu, ja kenelle + ;; Mukana myös signature joka vahvistetaan + accesstoken (get headerit "x-iam-accesstoken") + + ;; Sisältää käyttäjän käyttäjän tietoja, roolit, yhteystiedot, lxtunnus + ;; Mukana myös signature joka vahvistetaan + iam-data (get headerit "x-iam-data") + + ;; Subject ID (sub), eli käyttäjä kenelle JWT on myönnetty, tällä voidaan tunnistaa käyttäjä (mukana myös yllä olevissa tokeneissa) + ;; Tällä voidaan esim invalitoida token, kun käyttäjä kirjautuu ulos, mutta Harjassa ei tuollaista tarvetta kirjoitushetkellä taida olla + ; iam-identity (get headerit "x-iam-identity") + + ;; Vahvistetaan että tokenien payloadit on eheät + vahvistetut-tunnustiedot (if (and + iam-data + todennus-varmistus-paalla?) + (varmistus/vahvista-jwt-signaturet accesstoken iam-data kehitysmoodi? public-key-url) + (varmistus/tunnistetiedot iam-data)) - (let [headerit (select-keys headerit [;; Sisältää mm. Cogniton user poolin url:n ja app client id:n - "x-iam-accesstoken" - - ;; Sisältää käyttäjään liittyviä tietoja, mm. roolit - "x-iam-data" - - ;; Käyttäjän sub-kenttä Cognitossa - "x-iam-identity"]) - jwt (some-> - (get headerit "x-iam-data") - ;; Jaetaan kolmeen osaan: header, body, signature - (clojure.string/split #"\.")) - jwt-body (second jwt) - dekoodatut-headerit (some-> - ^String jwt-body - Base64/decodeBase64 - String. - cheshire/decode) ;; Käsittele vielä EntraID muodossa olevat roolit (json) - dekoodatut-headerit (update dekoodatut-headerit "custom:rooli" #(if (konv/onko-json? %) - (parsi-json-entraid-roolit %) - %))] + dekoodatut-headerit (update vahvistetut-tunnustiedot "custom:rooli" #(if (konv/onko-json? %) + (parsi-json-entraid-roolit %) + %))] ;; Mapataan Cognito-headerit vanhan mallisiksi vastaaviksi OAM-headereiksi ;; TODO: Siirrytään mahdollisesti myöhemmin käyttämään pelkkiä cognito-headereita @@ -157,6 +162,7 @@ "custom:organisaatio" "oam_organization" "custom:ytunnus" "oam_user_companyid"})))) + (defn- koka-headerit [headerit] (reduce-kv (fn [m k v] @@ -187,10 +193,10 @@ "Palauttaa headerit sellaisenaan, mikäli headereiden joukosta löytyy jokin OAM_-headeri. Muutoin, yritetään purkaa AWS Cognitolta saadut headerit, jotka mapataan OAM_-headereiksi ja lisätään muiden headereiden joukkoon." - [headerit] + [headerit kehitysmoodi? todennus-varmistus] (if (empty? (koka-headerit headerit)) (-> - (merge headerit (pura-cognito-headerit headerit)) + (merge headerit (pura-cognito-headerit headerit kehitysmoodi? todennus-varmistus)) (prosessoi-apikayttaja-header)) headerit)) @@ -298,26 +304,30 @@ (or (and oikeudet (oikeudet kayttaja)) koka-headerit)) -(defn koka->kayttajatiedot [db headerit oikeudet] - (let [headerit (prosessoi-kayttaja-headerit headerit) - oam-tiedot (ohita-oikeudet (koka-headerit headerit) oikeudet)] - (try - (get (swap! kayttajatiedot-cache-atom #(cache/through - (fn [oam-tiedot] - (varmista-kayttajatiedot db oam-tiedot)) - % - oam-tiedot)) - oam-tiedot) - (catch Throwable t - (log/error t "Käyttäjätietojen varmistuksessa virhe!"))))) +(defn koka->kayttajatiedot + ([db headerit oikeudet kehitysmoodi?] + (koka->kayttajatiedot db headerit oikeudet kehitysmoodi? nil)) + ([db headerit oikeudet kehitysmoodi? todennus-varmistus] + (let [headerit (prosessoi-kayttaja-headerit headerit kehitysmoodi? todennus-varmistus) + oam-tiedot (ohita-oikeudet (koka-headerit headerit) oikeudet)] + (try + (get (swap! kayttajatiedot-cache-atom #(cache/through + (fn [oam-tiedot] + (varmista-kayttajatiedot db oam-tiedot)) + % + oam-tiedot)) + oam-tiedot) + (catch Throwable t + (log/error t "Käyttäjätietojen varmistuksessa virhe!")))))) (defprotocol Todennus "Protokolla HTTP pyyntöjen käyttäjäidentiteetin todentamiseen." - (todenna-pyynto [this req] + (todenna-pyynto [this req kehitysmoodi?] "Todenna annetun HTTP-pyynnön käyttäjätiedot, palauttaa uuden req mäpin, jossa käyttäjän tiedot on lisätty avaimella :kayttaja.")) -(defrecord HttpTodennus [oikeudet] +(defrecord HttpTodennus + [oikeudet todennus-varmistus] component/Lifecycle (start [this] (log/info "Todennetaan HTTP käyttäjä KOKA headereista.") @@ -326,14 +336,14 @@ this) Todennus - (todenna-pyynto [{db :db :as this} req] + (todenna-pyynto [{db :db :as this} req kehitysmoodi?] (let [headerit (:headers req) kayttaja-id (headerit "oam_remote_user")] (if (nil? kayttaja-id) (do - (log/warn (str "Todennusheader oam_remote_user puuttui kokonaan" headerit)) + (log/warn (str "Todennusheader oam_remote_user puuttui kokonaan: " headerit)) (throw+ todennusvirhe)) - (if-let [kayttajatiedot (koka->kayttajatiedot db headerit oikeudet)] + (if-let [kayttajatiedot (koka->kayttajatiedot db headerit oikeudet kehitysmoodi? todennus-varmistus)] (assoc req :kayttaja kayttajatiedot) (do (log/warn (str @@ -350,14 +360,15 @@ this) Todennus - (todenna-pyynto [this req] + (todenna-pyynto [this req kehitysmoodi?] (assoc req :kayttaja kayttaja))) (defn http-todennus - ([] (http-todennus nil)) - ([oikeudet] - (->HttpTodennus oikeudet))) + ([] (http-todennus nil nil)) + ([oikeudet] (http-todennus oikeudet nil)) + ([oikeudet todennus-varmistus] + (->HttpTodennus oikeudet todennus-varmistus))) (defn feikki-http-todennus [kayttaja] (->FeikkiHttpTodennus kayttaja)) diff --git a/src/clj/harja/palvelin/komponentit/todennus_varmistus.clj b/src/clj/harja/palvelin/komponentit/todennus_varmistus.clj new file mode 100644 index 0000000000..dbf1aa4972 --- /dev/null +++ b/src/clj/harja/palvelin/komponentit/todennus_varmistus.clj @@ -0,0 +1,333 @@ +(ns harja.palvelin.komponentit.todennus-varmistus + "Authentikoinnin varmistus (JWT Tokenit)" + (:require [clojure.string :as str] + [clojure.core.cache :as cache] + [clojure.data.json :as json] + [cheshire.core :as cheshire] + [org.httpkit.client :as http] + [buddy.sign.jwt :as jwt] + [buddy.core.keys :as keys] + [taoensso.timbre :as log]) + (:import (org.apache.commons.codec.binary Base64) + [java.security KeyFactory] + [java.security.spec X509EncodedKeySpec] + [org.bouncycastle.util.io.pem PemReader] + [java.io StringReader])) + +;; Accesstokenin public key (jwk=json key set) +(defonce accesstoken-jwk-cache (atom nil)) +;; Iam-data (käyttäjäroolit) public key (PEM) +(defonce iam-data-pk-cache (atom nil)) +;; Public avainten päivitys intervalli minuuteissa +(def +public-key-cache-paivitys-min+ 120) +;; Pidetään käyttäjätietoja muistissa vartti +(def +kayttaja-varmistus-cache-min+ 15) +;; Annetaan Cognitolle GET kutsuun user-agent tietoja +(def user-agent-headers "HARJA/0.0.1-SNAPSHOT (JWT signature/harja.palaute@solita.fi)") + + +(defn tunnistetiedot + "Palauttaa dekoodatut tunnistetiedot käyttäjästä, sisältää roolit yms (x-iam-data) + Tämä palautetaan silloin kun käyttäjän todennus onnistui, ja jatketaan authentikointia" + [iam-data] + (some-> iam-data + (str/split #"\.") ;; HEADER.PAYLOAD.SIGNATURE + second ;; Meillä kiinnostaa tässä kohti vain payload + Base64/decodeBase64 + String. + cheshire/decode)) + + +(defn parsi-PEM-public-key + "Cognito antaa X-Iam-data public avaimen PEM muodossa + Se täytyy muuntaa sellaiseen java muotoon joka kelpaa signaturen tarkastukseen" + [pem-key] + (let [reader (PemReader. (StringReader. pem-key)) + pem-object (.readPemObject reader) + key-bytes (.getContent pem-object) + key-spec (X509EncodedKeySpec. key-bytes) + key-factory (KeyFactory/getInstance "EC")] ;; "RSA" RSA avaimille, "EC" ECDSA avaimille + (.generatePublic key-factory key-spec))) + + +(defn public-key-vanhentunut? + "Hakee uuden public keyn, jos sitä ei ole haettu vähään aikaan + Cognitolla on ominaisuus rotatoida public avaimia + Tämä tehdään myös sen yhteydessä, jos käyttäjän todennus epäonnistuu (eli avain mahdollisesti rotatoitunut)" + [cache-atom paivitys-intervalli] + (> (- (System/currentTimeMillis) (:fetched-at @cache-atom)) (* paivitys-intervalli 60 1000))) + + +(defn hae-public-key + "Tekee GET kutsun joka hakee public avaimen Cognitolta (x-iam-accesstoken & x-iam-data) + Jos avain on jo cachessa, palautetaan tallennettu arvo + Jos avaimet rotatoituu cachen aikana, ne päivitetään automaattisesti" + ([paivita? lx-kayttaja api-url PEM? cache-atom paivitys-intervalli] + (hae-public-key paivita? lx-kayttaja api-url PEM? cache-atom paivitys-intervalli false)) + ([paivita? lx-kayttaja api-url PEM? cache-atom paivitys-intervalli kehitysmoodi?] + (if + ;; Jos public avainta ei ole päivitetty x minuuttiin, päivitetään se + ;; Tällainen cachetus vielä, koska yksi backend, monta käyttäjää, Cognito ei rotatoi public avaimia kovin tiheään + (or + paivita? ;; Tämä on true jos käyttäjä ei päässyt sisälle, yritetään yhden kerran uudelleen + (nil? @cache-atom) + (public-key-vanhentunut? cache-atom paivitys-intervalli)) + (try + (let [_ (log/info + ;; TODO, logitusta tuotantoon, jotta nähdään cachen toimivuus + ;; Poistetaan myöhemmin + (str "Tehdään GET JWT public key kutsu, paivita? " paivita? " LX: " lx-kayttaja)) + + response @(http/get api-url {:headers {;; Lisää Cognitolle tiedoksi mistä pyyntö tulee + "User-Agent" (if paivita? + ;; Koska authentikointi epäonnistui, laita LX tunnus mukaan + (str user-agent-headers " (update public-key/" lx-kayttaja ")") + user-agent-headers) + "Content-Type" "application/json"}}) + + response (when-not kehitysmoodi? + (if PEM? + (-> response :body (slurp) (parsi-PEM-public-key)) + (-> response :body (json/read-json) :keys))) + + _ (reset! cache-atom {:key response :fetched-at (System/currentTimeMillis)})] + response) + (catch Exception e + ;; Jotain meni pieleen, ei haluta että näin käy (kriittinen osa). Laukaistaan slack-hälytys. + (log/error (str + "[JWT-ERROR] Public avaimen päivitys epäonnistui: " (.getMessage e) + " PEM?: " PEM? + " käyttäjä: " lx-kayttaja + " intervalli: " paivitys-intervalli + " URL: " api-url + " cache-atom: " @cache-atom)))) + ;; Public avaimet on ajan tasalla, palauta tallennettu arvo + (-> @cache-atom :key)))) + + +(defn hae-ja-suodata-accesstoken-public-key + "Kutsuttu rajapinta palauttaa Accesstokenin public avaimen + Mukana on 2 avainta, joten suodatetaan vaan haluttu avain (key identifier)" + [kid issuer paivita? lx-kayttaja] + (some #(when (= kid (:kid %)) %) + (hae-public-key + paivita? + lx-kayttaja + (str issuer "/.well-known/jwks.json") + false + accesstoken-jwk-cache + +public-key-cache-paivitys-min+))) + + +(defn- decode-base64-json [s] + (-> s Base64/decodeBase64 String. (json/read-json))) + + +(defn dekoodaa-token + "Dekoodaa enkoodatut headerit, ja palauttaa tiedot json stringinä" + [token] + (let [[header payload signature] (if (> (count token) 2) (str/split token #"\.") [nil nil nil])] + {:header (some-> header decode-base64-json) + :payload (some-> payload decode-base64-json) + :signature signature})) + + +(defn varmista-jwt-tokenit + "Varmistaa authentikoinnin oikellisuuden Cogniton antamista tokeneista + Tokenit mitkä vahvistetaan: x-iam-accesstoken (tokenin metadata), x-iam-data (käyttäjän tiedot&roolit) + https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-manually-inspect" + [accesstoken iam-data paivita? public-key-url kehitysmoodi?] + (let [lx-kayttaja (-> iam-data dekoodaa-token :payload :custom:uid) + accesstoken-header (-> accesstoken dekoodaa-token :header) + accesstoken-kid (:kid accesstoken-header) + accesstoken-issuer (-> accesstoken dekoodaa-token :payload :iss) + iam-data-header (-> iam-data dekoodaa-token :header) + iam-data-kid (:kid iam-data-header) + ;; Hae accesstoken (tokenin metadata väitteet) public avain + accesstoken-public-key (if kehitysmoodi? + ;; Kehitysmoodissa passataan mock avaimet suoraa, eikä tehdä GET kutsuja + (first public-key-url) + ;; Tuotantoympäristössä, haetaan oikea avain GET kutsulla + (hae-ja-suodata-accesstoken-public-key accesstoken-kid accesstoken-issuer paivita? lx-kayttaja)) + ;; Muunna java muotoon, joka käy javan signature librarylle + accesstoken-public-key (keys/jwk->public-key accesstoken-public-key) + ;; Hae iam-data (jossa käyttäjärooli väitteet) public avain, tämä on PEM muodossa + iam-data-public-key (if kehitysmoodi? + ;; Kehitysmoodissa passataan mock avaimet suoraa, eikä tehdä GET kutsuja + (second public-key-url) + ;; Tuotantoympäristössä, haetaan oikea avain GET kutsulla + (hae-public-key + paivita? + lx-kayttaja + (str public-key-url iam-data-kid) + true + iam-data-pk-cache + +public-key-cache-paivitys-min+)) + ;; Avainten algoritmit, buddy kirjasto haluaa nämä lowercasena + accesstoken-algoritmi (-> accesstoken-header :alg (str/lower-case) (keyword)) + iam-data-algoritmi (-> iam-data-header :alg (str/lower-case) (keyword)) + + ;; Nämä on arvokkaita testauksessa ja voi jättää toistaiseksi tähän + #_ #_ _ (when kehitysmoodi? + (println "\n accesstoken payload: " (-> accesstoken dekoodaa-token :payload)) + (println "\n accesstoken header: " (-> accesstoken dekoodaa-token :header)) + (println "\n accesstoken signature: " (-> accesstoken dekoodaa-token :signature)) + (println "\n accesstoken-algoritmi: " accesstoken-algoritmi) + (println "\n accesstoken-public-key: " accesstoken-public-key) + + (println "\n iam-data payload: " (-> iam-data dekoodaa-token :payload)) + (println "\n iam-data header: " (-> iam-data dekoodaa-token :header)) + (println "\n iam-data signature: " (-> iam-data dekoodaa-token :signature)) + (println "\n iam-data-algoritmi: " iam-data-algoritmi) + (println "\n iam-data-public-key: " iam-data-public-key)) + + ;; Verifioi tokenit kutsumalla unsign, joka tarkastaa saapuvien tietojen allekirjoituksen + ;; Signaturen verifiointi tulee suoraan javalta (java.security.Signature), niitä ei clojurena ole suoraa näkyvillä + _ (jwt/unsign iam-data iam-data-public-key {:alg iam-data-algoritmi}) ;; Sisältää käyttäjän tietoja & Roolit + _ (jwt/unsign accesstoken accesstoken-public-key {:alg accesstoken-algoritmi}) ;; Sisältää mm. user poolin url, client idt + _ (log/info + ;; TODO, logitusta tuotantoon jotta nähdään toimivuus, tämän voi myöhemmin poistaa, mergetään toistaiseksi näin + "Todennettiin onnistuneesti (JWT): " + (str + (-> iam-data dekoodaa-token :payload :custom:etunimi) + " - " + (-> iam-data dekoodaa-token :payload :custom:uid)))])) + + +(defn- authentikointi-epaonnistui + "Käsitellään virhe, ohjaa 'Ei käyttöoikeutta' näkymään + + - Pääsääntöisesti olemassa kirjautumistietojen eheyden tarkistukseen + - Virheet johtuvat yleensä järjestelmävirheestä, joko Cognitossa, tai muualla järjestelmissä + - Hyvin epätodennäköistä, mutta mahdollista, että payloadia sorkittu ja tehty hyökkäysyritys -> tutki mitä logeissa lukee + + - Tokenit kulkee AWS(Välittäjä) backendistä Harja backendiin, se että niitä pääsee sorkkimaan, pitää mennä monta asiaa pieleen + - JWT ei taida Harjassa olla frontissa ikinä näkyvillä, ei edes enkoodattuna, liikkuvat frontissa luultavasti encrypted AWS session cookiena + (? fact check ?) + + Mahdollisia syitä: + + 'Message seems manipulated': + - Public avain voi olla väärin vaikka yritettiin hakea uusi (Ota Harjan kirjautumisen välittäjälle yhteyttä) + - Voi olla että kirjautumisen välittäjän Cognito juntturassa, katso mitä logissa lukee + + 'Token is expired' + - Token mennyt vanhaksi, tätä ei pitäisi ikinä näkyä (välittäjälle yhteyttä) + - Käyttäjän kirjautuminen on Harjan cachessa ainakin vartin, jonka jälkeen Cogniton pitäisi antaa uusi token automaattisesti + + 'Audience does not match' + - Audience, eli järjestelmän tunnus mille token annettu, ei jostain syystä täsmää (välittäjälle yhteyttä) + + 'The subject does not match' + - Subjekti (käyttäjä), kenelle token myönnetty, ei jostain syystä täsmää (välittäjälle yhteyttä) + + 'Issuer does mot match' + - Tokenin välittäjä ei jostain syystä täsmää (taas, välittäjälle yhteyttä) + + 'x-iam-data ei sisältänyt payloadia' + - Itse selitteinen, ei pitäisi näkyä ikinä, välittäjältä pitäisi tulla aina payload, ole heille yhteydessä + - Todennusta on voitu yrittää manuaalisesti kutsua invalid tokenilla + + 'JWT Token puuttui kokonaan' + - Todennusta on yritetty manuaalisesti kutsua invalid tokenilla + + 'Public avaimen päivitys epäonnistui' + - Public avaimen GET kutsussa meni jotain pieleen, katso logeja + - Public avain ympäristömuuttuja voi sisältää väärän tai invalidin linkin + - Mahdollisesti välittäjälle yhteyttä, jos heidän päässä vika (GET kutsut tehdään Cognitoon) + - Ei voida päästää Harjaan, koska jos hyökkääjä ottaa payloadista esim issuerin pois, virhe triggeröityy" + + [e iam-data accesstoken] + + ;; 'JWT-ERROR' Laukaisee slack-hälytyksen, nämä halutaan tutkia aina + (log/error (str "[JWT-ERROR] Authentikointi ei onnistunut: " (.getMessage e))) + + ;; Header / tokenin metadata + (log/error (str "Saatu JWT header (iam-data): " (-> iam-data dekoodaa-token :header))) + ;; Tässä on käyttäjän yritetyt roolit yms, jos logeilta löytyy roolissa jotain outoa, tee välittömästi toimenpiteitä + (log/error (str "Saatu JWT payload (iam-data): " (-> iam-data dekoodaa-token :payload))) + + ;; Logitetaan vielä accesstoken, koska täällä voi myös jotain olla pielessä + (log/error (str "Accesstoken header: " (-> accesstoken dekoodaa-token :header))) + (log/error (str "Accesstoken payload: " (-> accesstoken dekoodaa-token :payload))) + + ;; Tämä näkyy slack-hälytyksen mukana suorana ohjeena, kun menet hälytyksen cloudwatch linkkiin + ;; Halutaan suora ohjeistus sekä toiminnallisuus saada varmistus nopeasti kiinni + ;; Kun varmistuksen laittaa kiinni, käyttäjät todennetaan normaalisti Harjaan, mutta ilman tokenin varmistusta + (log/error "HÄTÄ FIX: Jos käyttäjät jää ilman hyvää syytä ulos, aseta TODENNUS_VARMISTUS_PAALLA false -> depoly configs [infra]") + + ;; Authentikointi ei mennyt läpi, poista kaikki oikeudet käyttäjältä + ;; TODO.. Ei laiteta blokkausta vielä päälle tuotantoon, mergetään toistaiseksi näin. + ;; + ; "failed" uudelleenohjaa "Authentikaatio epäonnistui" näkymään + ; (-> (tunnistetiedot iam-data) (assoc "custom:rooli" "failed")) + + ;; TODO.. + ;; Palauta tunnistetiedot toistaiseksi, ja jatka todennusta normaalisti, vaikka virhe tapahtui + ;; Blokkaus päälle vasta sitten kun tämä on todettu tuotannossa toimivaksi + (tunnistetiedot iam-data)) + + +;; Pidä käyttäjän tietoja tallessa x ajan, todennusta kutsutaan liian tiheästi muuten +(def kayttaja-varmistettu-cache (atom (cache/ttl-cache-factory {} :ttl (* +kayttaja-varmistus-cache-min+ 60 1000)))) + +(defn vahvista-jwt-signaturet + "Tekee JWT tokenien signaturen vahvistuksen + Palauttaa dekoodatut tunnistetiedot, mikäli signaturet OK + JWT (Json Web Token) sisältää pisteellä erotetut osiot (enkoodattuna): HEADER.PAYLOAD.SIGNATURE + + Tokenien osiot: + Header: Metadata + Payload: Claims (väitteet, mitkä pitää todentaa oikeiksi) + Signature: Kryptografinen allekirjoitus Cognitolta, perustuu alkuperäiseen header.payload hash-arvoon + + 1. Haetaan ja parsitaan public avaimet, joita käytetään signaturen vahvistukseen + X-Iam-accesstoken public avaimen linkki on headereissa sisällä :iss (issuer) + X-Iam-data public avain on erillinen staattinen linkki, joka on PEM muodossa (passataan asetukset.edn kautta) + + 2. Lasketaan odotettu hash, ja verrataan alkuperäiseen signaturen hashiin (jwt/unsign) + Täällä myös katsotaan tokenin expiration yms, virhe heitetään jos mitään on väärin + 3. Jos kaikki OK, palautetaan dekoodattu tunnusdata, ja jatketaan authentikointia + 4. Tuloksena käyttäjän roolitiedot on vahvistettu oikeiksi, eikä mitään ole sorkittu matkalla" + ([accesstoken iam-data kehitysmoodi? public-key-url] + ;; Kutsu todennuksen kautta tulee aina tähän + (if + ;; Tarkistetaan signaturet kun vastaanotetaan validi pyyntö + (and accesstoken iam-data) + ;; yrita-uudelleen? defaulttina aina false + (vahvista-jwt-signaturet accesstoken iam-data kehitysmoodi? public-key-url false) + (authentikointi-epaonnistui (Exception. "JWT Token puuttui kokonaan! Molemmat x-iam-accesstoken sekä x-iam-data vaaditaan!") iam-data accesstoken))) + ([accesstoken iam-data kehitysmoodi? public-key-url yrita-uudelleen?] + (let [cache-key + ;; Otetaan avaimesta pelkästään käyttäjän tiedot, jotka on suht staattisia, tallenna ne cacheen + (-> iam-data dekoodaa-token :payload (some-> (dissoc :iss :sub :exp)))] + (get (swap! kayttaja-varmistettu-cache #(cache/through + (fn [_] + (try + ;; Payloadia ei ollut tokenissa mukana, jotain on huonosti, laukaisee slack-hälytyksen, ja estää pääsyn + (when-not cache-key + (throw (Exception. "x-iam-data ei sisältänyt payloadia ollenkaan!"))) + + (if public-key-url + (varmista-jwt-tokenit accesstoken iam-data yrita-uudelleen? public-key-url kehitysmoodi?) + ;; Laukaisee slack-hälytyksen, mutta päästää käyttäjän sisään, koska authentikointia ei voida tehdä + (log/error "[JWT-ERROR] Authentikointia ei voida tehdä, public-key-url ei ole asetettu! Aseta public-key-url ympäristömuuttuja.")) + + ;; Palauta tiedot ja jatka authentikointia jos onnistui / public-key muuttuja puuttui + (tunnistetiedot iam-data) + + ;; Todennus tehtiin ja se epäonnistui + (catch Exception e + (if + (and + (not yrita-uudelleen?) + (= (.getMessage e) "Message seems corrupt or manipulated")) + ;; Tarkista, onko public key rotatoitunut yrittämällä uudelleen + (vahvista-jwt-signaturet accesstoken iam-data kehitysmoodi? public-key-url true) + ;; Public key on ajan tasalla, ja vieläkin tulee virhe, heitetään hälytys logiin + (authentikointi-epaonnistui e iam-data accesstoken))))) + ;; Tiedot on jo cachessa + % + ;; Tallenna cacheen käyttäjän tiedot, pysyvät siellä 15 min ->> +kayttaja-varmistus-cache-min+ + cache-key)) + cache-key)))) diff --git a/src/clj/harja/palvelin/komponentit/tuck_remoting.clj b/src/clj/harja/palvelin/komponentit/tuck_remoting.clj index 4e5606c38d..d39d2e2e1c 100644 --- a/src/clj/harja/palvelin/komponentit/tuck_remoting.clj +++ b/src/clj/harja/palvelin/komponentit/tuck_remoting.clj @@ -55,7 +55,7 @@ (fn tuck-remoting-handler [request] (let [konteksti @konteksti-atomi client-id (str (UUID/randomUUID)) - kayttaja (todennus/koka->kayttajatiedot db (:headers request) koka-ohitusoikeudet)] + kayttaja (todennus/koka->kayttajatiedot db (:headers request) koka-ohitusoikeudet true)] ;; https://http-kit.github.io/server.html#websocket (with-channel request kanava (on-close kanava (fn [status] diff --git a/src/clj/harja/palvelin/main.clj b/src/clj/harja/palvelin/main.clj index 0047b13352..602de95dbc 100644 --- a/src/clj/harja/palvelin/main.clj +++ b/src/clj/harja/palvelin/main.clj @@ -214,7 +214,8 @@ (log/info "Asetettiin clojure.async thread-poolin koko: " (Long/getLong "clojure.core.async.pool-size"))) (defn luo-jarjestelma [asetukset] - (let [{:keys [tietokanta tietokanta-replica http-palvelin kehitysmoodi]} asetukset] + (let [{:keys [tietokanta tietokanta-replica http-palvelin + kehitysmoodi todennus-varmistus sahke-headerit]} asetukset] (component/system-map :metriikka (metriikka/luo-jmx-metriikka) :db (tietokanta/luo-tietokanta (assoc tietokanta @@ -231,21 +232,20 @@ kehitysmoodi) :todennus (component/using - (todennus/http-todennus (:sahke-headerit asetukset)) + (todennus/http-todennus sahke-headerit todennus-varmistus) [:db]) :http-palvelin (component/using - (http-palvelin/luo-http-palvelin http-palvelin - kehitysmoodi) + (http-palvelin/luo-http-palvelin http-palvelin kehitysmoodi todennus-varmistus) [:todennus :metriikka :db]) ;; FIXME: Tuck-remoting otettu toistaiseksi pois testikäytöstä kokonaan, koska se ei toimi kunnolla #_#_:tuck-remoting (component/using - (tuck-remoting/luo-tuck-remoting (:sahke-headerit asetukset)) - [:http-palvelin :db]) + (tuck-remoting/luo-tuck-remoting (:sahke-headerit asetukset)) + [:http-palvelin :db]) ;; Tuck-remoting palvelu ilmoitusten välittämiseen WebSocketin yli #_#_:ilmoitukset-ws-palvelu (component/using - (ilmoitukset-ws/luo-ilmoitukset-ws) - [:tuck-remoting :db]) + (ilmoitukset-ws/luo-ilmoitukset-ws) + [:tuck-remoting :db]) :pdf-vienti (component/using (pdf-vienti/luo-pdf-vienti) @@ -544,8 +544,8 @@ [:http-palvelin :db :excel-vienti]) :kustannukset (component/using - (kustannukset-palvelu/->Kustannukset) - [:http-palvelin :db]) + (kustannukset-palvelu/->Kustannukset) + [:http-palvelin :db]) :tyomaapaivakirja (component/using (tyomaapaivakirja-palvelu/->Tyomaapaivakirja (:kehitysmoodi asetukset)) @@ -828,12 +828,12 @@ (component/using (tarjoushinnat-hallinta/->TarjoushinnatHallinta) [:http-palvelin :db]) - + :lupaukset-hallinta (component/using (lupaukset-hallinta/->LupauksetHallinta) [:http-palvelin :db]) - + :paallystysilmoitukset-hallinta (component/using (paallystysilmoitukset-hallinta/->PaallystysilmoituksetHallinta) diff --git a/src/cljs/harja/views/main.cljs b/src/cljs/harja/views/main.cljs index f633885dce..3be9f0ab72 100644 --- a/src/cljs/harja/views/main.cljs +++ b/src/cljs/harja/views/main.cljs @@ -263,6 +263,10 @@ (empty? (:urakkaroolit kayttaja)) (empty? (:organisaatioroolit kayttaja))))) +(defn authentikointi-epaonnistui? [kayttaja] + ;; Tarkoittaa että authentikointi epäonnistui, tästä laukaistaan myös slack häly: JWT-ERROR + (boolean (contains? (:roolit kayttaja) "failed"))) + (defn kuuntele-oikeusvirheita [] (t/kuuntele! :ei-oikeutta (fn [tiedot] (viesti/nayta! (:viesti tiedot) @@ -285,9 +289,16 @@ [:div "Harjan käyttö aikakatkaistu kahden tunnin käyttämättömyyden takia. Lataa sivu uudelleen."] (if (nil? kayttaja) [ladataan] - (if (ei-kayttooikeutta? kayttaja) + (cond + (authentikointi-epaonnistui? kayttaja) + [:div.ei-kayttooikeutta-wrap + [:img#harja-brand-icon {:src "images/harja_logo_soft.svg"}] + [:div.ei-kayttooikeutta "Authentikointi epäonnistui. Ei käyttöoikeutta Harjaan."]] + + (ei-kayttooikeutta? kayttaja) [:div.ei-kayttooikeutta-wrap [:img#harja-brand-icon {:src "images/harja_logo_soft.svg"}] [:div.ei-kayttooikeutta "Ei käyttöoikeutta Harjaan. Ota yhteys organisaatiosi käyttövaltuusvastaavaan."]] - [paasisalto sivu korkeus])))) + + :else [paasisalto sivu korkeus])))) [ladataan])))) diff --git a/test/clj/harja/palvelin/komponentit/fim_test.clj b/test/clj/harja/palvelin/komponentit/fim_test.clj index 903edc1417..f9c87fc8ae 100644 --- a/test/clj/harja/palvelin/komponentit/fim_test.clj +++ b/test/clj/harja/palvelin/komponentit/fim_test.clj @@ -1,7 +1,5 @@ (ns harja.palvelin.komponentit.fim-test - (:require [harja.palvelin.komponentit.todennus :as todennus] - [harja.domain.oikeudet :as oikeudet] - [harja.testi :refer :all] + (:require [harja.testi :refer :all] [clojure.test :as t :refer [deftest is use-fixtures testing]] [com.stuartsierra.component :as component] [harja.palvelin.komponentit.tietokanta :as tietokanta] diff --git a/test/clj/harja/palvelin/komponentit/todennus_jwt_authentikointi_test.clj b/test/clj/harja/palvelin/komponentit/todennus_jwt_authentikointi_test.clj new file mode 100644 index 0000000000..023460dafd --- /dev/null +++ b/test/clj/harja/palvelin/komponentit/todennus_jwt_authentikointi_test.clj @@ -0,0 +1,586 @@ +(ns harja.palvelin.komponentit.todennus-jwt-authentikointi-test + "Todennuksen JWT signaturen testit (Ei tee GET kutsuja ulos) + Simuloi kuinka Tuotannossa saadaan tokenit, varmistaen niiden signaturen käyttäjän tullessa Harjaan + Testaa todennuksen varmistuksen kaikki komponentit" + (:require [buddy.sign.jwt :as jwt] + [clojure.core.cache :as cache] + [clj-time.core :as time] + [clj-time.coerce :as coerce] + [clojure.string :as str] + [harja.palvelin.komponentit.todennus :as todennus] + [harja.domain.oikeudet :as oikeudet] + [harja.testi :refer :all] + [taoensso.timbre :as log] + [cheshire.core :as cheshire] + [harja.palvelin.komponentit.http-palvelin :as http-palvelin] + [harja.palvelin.komponentit.todennus-varmistus :as jwt-varmistus] + [clojure.test :as t :refer [deftest is use-fixtures testing]] + [com.stuartsierra.component :as component] + [harja.palvelin.komponentit.tietokanta :as tietokanta]) + + (:import (org.apache.commons.codec.binary Base64) + (org.bouncycastle.openssl.jcajce JcaPEMWriter) + (java.io StringWriter) + java.security.KeyPairGenerator)) + +(def timbre-log-historia (atom [])) + +(log/merge-config! + {:appenders + {:memory {:enabled? true + :fn (fn [{:keys [msg_]}] + (swap! timbre-log-historia conj (force msg_)))}}}) + +(defn jarjestelma-fixture [testit] + (alter-var-root + #'jarjestelma + (fn [_] + (component/start + (component/system-map + :db (tietokanta/luo-tietokanta testitietokanta) + :todennus (component/using + (todennus/http-todennus) + [:db]))))) + (testit) + (alter-var-root #'jarjestelma component/stop)) + +(use-fixtures :each jarjestelma-fixture) + +(defn- generoi-expiration + [minuutit] + (-> (time/now) (time/plus (time/minutes minuutit)) (coerce/to-long) (/ 1000) long)) + +(def mock-expiration (generoi-expiration 1080)) +(def mock-issued-at (-> (time/now) (coerce/to-long) (/ 1000) long)) + +(def mock-key-identifier "tcSe4a9TGsOM7Gsym0E6UL3") +(def mock-subject "d5-1cff-4ebb-a857-07e11") +(def mock-https-issuer "mock-issuer-https-link") +(def mock-jwt-id "afe99d-b453-4c3e-975d-b66cb") +(def mock-client-id "tc20d3i4ghv94ks0semt4") +(def mock-signer "arn:aws:elasticloadbalancing:region-id:010101:loadbalancer/app/Vayla2-TEST-ALB/123456") +(def mock-username "vayla12ctestoam_testi.testi@sahkop.fi") + +(def mock-email "testi.testi@at.fi") +(def mock-puh "040123") +(def mock-ytunnus "133-7") +(def mock-etunimi "Jani") +(def mock-uid "LX123") +(def mock-organisaatio "Meitsin organisaatio") +(def mock-sukunimi "Pasatronic") +(def mock-roolit "Extranet_Kayttaja,all_ex_users,ext_kayttajat,1242141-OULU2_vastuuhenkilo,Jarjestelmavastaava,4242523-AES4_vastuuhenkilo,MHU-TESTI-LAP-ROV_Laadunvalvoja") + +(def odotetut-roolit (todennus/kayttajan-roolit + (partial hae-urakan-id-sampo-idlla) + (partial hae-urakoitsijan-id-ytunnuksella) + oikeudet/roolit + mock-roolit)) + +(def test-accesstoken-jwt + ;; JWT accesstoken mock dataa, jossa header, payload, ja signature + ;; Signature generoidaan jälkeen, en tiedä vielä miten + [{:kid mock-key-identifier, :alg "RS256"} ;; header + + {:sub mock-subject, + :iss mock-https-issuer, + :exp mock-expiration, + :username mock-username, + :scope "openid", + :cognito:groups "[region-id_Vayla12cTestOAM]", + :token_use "access", :auth_time mock-expiration, + :jti mock-jwt-id, + :client_id mock-client-id, + :version "2", + :iat mock-issued-at} ;; payload + "signature=="]) + +(def test-iam-data-jwt + ;; JWT iam-data (käyttäjätiedot & roolit) mock dataa, jossa header, payload, ja signature + [{:typ "JWT", + :alg "ES256", + ;; Iam datalla ei tarvi tähän key identifieriä, public key haetaan hieman erillä lailla + :iss mock-https-issuer, + :client mock-client-id, + :signer mock-signer, + :exp mock-expiration} ;; header + + {:email mock-email, + :custom:puhelin mock-puh, + :custom:ytunnus mock-ytunnus, + :sub mock-subject, + :iss mock-https-issuer, + :exp mock-expiration, + :username mock-username, + :custom:rooli mock-roolit, + :custom:etunimi mock-etunimi, + :email_verified "false", + :custom:uid mock-uid, + :custom:organisaatio mock-organisaatio, + :custom:sukunimi mock-sukunimi} ;; payload + "signature=="]) + + +(defn- _log_ [str debug?] + (when debug? (println "\n") (log/info str))) + + +(defn- nollaa-todennuksen-cache + "Jokaisessa testissä halutaan nollata todennuksen cache, kun käytetään samoja mock tietoja" + [] + (reset! timbre-log-historia []) + (reset! jwt-varmistus/iam-data-pk-cache nil) + (reset! jwt-varmistus/accesstoken-jwk-cache nil) + (reset! todennus/kayttajatiedot-cache-atom (cache/ttl-cache-factory {} :ttl (* 15 60 1000))) + (reset! jwt-varmistus/kayttaja-varmistettu-cache (cache/ttl-cache-factory {} :ttl (* 15 60 1000)))) + + +(defn- atomi-sisaltaa-stringin? [atom string] + (boolean (some #(str/includes? (str %) string) @atom))) + + +(defn- public-key-to-pem + "Muunna ec iam-data public avain PEM muotoon + Tämä simuloi miten se saadaan Cognitolta tuotannossa" + [public-key] + (let [sw (StringWriter.) + pw (JcaPEMWriter. sw)] + (.writeObject pw public-key) + (.flush pw) + (.close pw) + (.toString sw))) + + +(defn- rsa-public-key-to-jwks + "Muunna RSA avain vielä jwks muotoon (json web key set) + Tämä simuloi miten se saadaan Cognitolta tuotannossa" + [rsa-public-key key-id] + (let [modulus (.getModulus rsa-public-key) + exponent (.getPublicExponent rsa-public-key)] + {:alg "RS256" + :e (-> exponent + (.toByteArray) + (Base64/encodeBase64URLSafeString)) + :kid key-id + :kty "RSA" + :n (-> modulus + (.toByteArray) + (Base64/encodeBase64URLSafeString)) + :use "sig"})) + + +(defn- generate-ec-key-pair + "Generoi private sekä public avaimet EC 256 bit + Tarvitaan mock JWT payloadin (iam-data) signaturen tekoon, jos onnistuu" + [] + (let [key-gen (KeyPairGenerator/getInstance "EC")] + (.initialize key-gen 256) + (.generateKeyPair key-gen))) + + +(defn- generate-rsa-key-pair + "Generoi private sekä public avaimet RSA 2048 bit + Tarvitaan mock JWT payloadin (accesstoken) signaturen tekoon, jos onnistuu" + [] + (let [key-gen (KeyPairGenerator/getInstance "RSA")] + (.initialize key-gen 2048) + (.generateKeyPair key-gen))) + + +(defn- generoi-jwt-signaturella + "Palauttaa JWT:n validilla signaturella käyttämällä mock tokeneja, sekä private avainta" + [private-key payload header alg] + (jwt/sign payload private-key {:alg alg :header header})) + + +(defn- tarkista-cognito-todennus-perustiedot [vastaus x-iam-accesstoken x-iam-data] + ;; Nämä tiedot pitäisi palautua, jos ei palaudu, jotain on todennuksessa vialla + (is (= (get-in vastaus [:kayttaja :sukunimi]) mock-sukunimi)) + (is (= (get-in vastaus [:kayttaja :etunimi]) mock-etunimi)) + (is (= (get-in vastaus [:kayttaja :kayttajanimi]) mock-uid)) + (is (= (get-in vastaus [:kayttaja :sahkoposti]) mock-email)) + (is (= (get-in vastaus [:kayttaja :puhelin]) mock-puh)) + + (is (= (get-in vastaus [:headers "sub"]) mock-subject)) + (is (= (get-in vastaus [:headers "exp"]) mock-expiration)) + (is (= (get-in vastaus [:headers "iss"]) mock-https-issuer)) + (is (= (get-in vastaus [:headers "username"]) mock-username)) + (is (= (get-in vastaus [:headers "oam_remote_user"]) mock-uid)) + (is (= (get-in vastaus [:headers "oam_user_last_name"]) mock-sukunimi)) + (is (= (get-in vastaus [:headers "oam_organization"]) mock-organisaatio)) + (is (= (get-in vastaus [:headers "oam_user_mail"]) mock-email)) + (is (= (get-in vastaus [:headers "oam_user_first_name"]) mock-etunimi)) + (is (= (get-in vastaus [:headers "oam_user_companyid"]) mock-ytunnus)) + + ;; JWT tokenit + (is (= (get-in vastaus [:headers "x-iam-accesstoken"]) x-iam-accesstoken)) + (is (= (get-in vastaus [:headers "x-iam-data"]) x-iam-data)) + (is (= (get-in vastaus [:headers "x-iam-identity"]) mock-subject))) + + +(defn- palauta-cognito-todennuspyynto-vastaus [kehitysmoodi? public-key todennus-varmistus-paalla? todennuspyynto] + (let [kehitysmoodi? kehitysmoodi? + handler (-> + (fn [req] req) + (http-palvelin/wrap-with-common-wrappers + kehitysmoodi? + {:public-key-url public-key :todennus-varmistus-paalla? todennus-varmistus-paalla?})) + todenna #(todennus/todenna-pyynto (:todennus jarjestelma) % kehitysmoodi?)] + (todenna (handler {:headers todennuspyynto})))) + + +(defn- initialisoi-cognito-jwt-todennuspyynto + "Generoi private & public avaimet, ja kirjoittaa tokeneille validin signaturen + Palauttaa validin todennuspyynnön payloadin Cognito muodossa" + ([] (initialisoi-cognito-jwt-todennuspyynto false)) + ([debug?] + (nollaa-todennuksen-cache) + (let [ec-key-pair (generate-ec-key-pair) + ec-private-key (.getPrivate ec-key-pair) + ec-public-key (.getPublic ec-key-pair) + _ (_log_ (str "EC key priv: " ec-private-key) debug?) + _ (_log_ (str "EC key pub: " ec-public-key) debug?) + + ;; Tässä on simuloituna iam-data public avain, joka saadaan Cognitolta PEM muodossa + ;; Tämä kyllä muunnetaan takaisin todennuksessa java muotoon, mutta testi kertoo jos todennus hajoaa + ec-public-key-PEM (public-key-to-pem ec-public-key) + ec-public-key-PEM (jwt-varmistus/parsi-PEM-public-key ec-public-key-PEM) + _ (_log_ (str "EC key JAVA: " ec-public-key-PEM) debug?) + + ;; Accesstokenin RSA avaimet + rsa-key-pair (generate-rsa-key-pair) + rsa-private-key (.getPrivate rsa-key-pair) + rsa-public-key (.getPublic rsa-key-pair) + + ;; Nämäkin pitää kiikutella oikeaan jwks muotoon, jollai Cognito niitä tarjoaa + rsa-jwks (rsa-public-key-to-jwks rsa-public-key mock-key-identifier) + _ (_log_ (str "RSA rsa-jwks: " rsa-jwks) debug?) + + accesstoken-payload (second test-accesstoken-jwt) + accesstoken-header (first test-accesstoken-jwt) + iam-data-payload (second test-iam-data-jwt) + iam-data-header (first test-iam-data-jwt) + + ;; Allekirjoita payloadit, palauttaa kokonaisen JWT:n - header.payload.sig + x-iam-accesstoken (generoi-jwt-signaturella rsa-private-key accesstoken-payload accesstoken-header :rs256) + x-iam-data (generoi-jwt-signaturella ec-private-key iam-data-payload iam-data-header :es256) + _ (_log_ (str "mock-expiration: " mock-expiration " issued at: " mock-issued-at) debug?)] + + ;; Palauta oikeanlainen todennuspyyntö, sisältää 2 JWT tokenia (identityä ei käytetä kirjoitushetkellä) + {:todennuspyynto {"x-iam-accesstoken" x-iam-accesstoken + "x-iam-data" x-iam-data + "x-iam-identity" mock-subject} + + :accesstoken-public-key rsa-jwks + :iam-data-public-key ec-public-key-PEM}))) + + +(deftest cognito-jwt-todennuspyynto-toimii + (let [initialisoidut-tiedot (initialisoi-cognito-jwt-todennuspyynto) + todennuspyynto (-> initialisoidut-tiedot :todennuspyynto) + iam-data-public-key (-> initialisoidut-tiedot :iam-data-public-key) + accesstoken-public-key (-> initialisoidut-tiedot :accesstoken-public-key) + ;; JWT tokenit + x-iam-data (get todennuspyynto "x-iam-data") + x-iam-accesstoken (get todennuspyynto "x-iam-accesstoken") + + kehitysmoodi? true + todennus-varmistus-paalla? true + public-key [accesstoken-public-key iam-data-public-key] + ;; Tekee pyynnön Harjan todennukseen + vastaus (palauta-cognito-todennuspyynto-vastaus kehitysmoodi? public-key todennus-varmistus-paalla? todennuspyynto) + + odotetut-harja-roolit (:roolit odotetut-roolit) + odotetut-urakka-roolit (:urakkaroolit odotetut-roolit) + mock-roolit-set (set (str/split mock-roolit #",")) + sisaltaa-roolin? (fn [role] + (some #(str/includes? % role) mock-roolit-set)) + vastaus-roolit (get-in vastaus [:kayttaja :roolit]) + vastaus-urakkaroolit (get-in vastaus [:kayttaja :urakkaroolit])] + + + (is (true? (atomi-sisaltaa-stringin? timbre-log-historia "Todennettiin onnistuneesti"))) + + ;; Vastaahan roolit odotettuja rooleja + (is (= vastaus-roolit odotetut-harja-roolit)) + (is (= vastaus-urakkaroolit odotetut-urakka-roolit)) + + ;; Katsotaan että cognito parsinta palauttaa kaikki roolit + ;; Tarkista että vastauksen harja-rooli löytyy passatuista rooleista + (is (every? mock-roolit-set vastaus-roolit) "Vastaus sisältää lähetetyt roolit") + + ;; Tarkista että vastauksen urakkaroolit löytyy passatuista rooleista + ;; {4 #{"vastuuhenkilo"} .. } <- vastuuhenkilo löytyy mock-rooli sisältä + ;; + ;; urakka id voisi myös tarkistaa, mutta lienee tarpeeksi hyvä tämä laiska check + (is (every? sisaltaa-roolin? (set (mapcat second vastaus-urakkaroolit))) "Vastaus sisältää lähetetyt urakkaroolit") + + ;; Roolit vastaa annettuja rooleja + (is (= (get-in vastaus [:headers "oam_groups"]) mock-roolit) "Käyttäjällä on roolit (critical)") + + ;; Perustiedot täsmää + (tarkista-cognito-todennus-perustiedot vastaus x-iam-accesstoken x-iam-data))) + + +;; TODO +;; Tämä päälle vasta sitten kun väännetään käyttäjien esto päälle +;; Aluksi mergetään ilman, joten tämä testi ei tule menemään läpi vielä +#_(deftest cognito-esta-harjaan-paasy-test + (let [initialisoidut-tiedot (initialisoi-cognito-jwt-todennuspyynto) + todennuspyynto (-> initialisoidut-tiedot :todennuspyynto) + x-iam-data (get todennuspyynto "x-iam-data") + x-iam-accesstoken (get todennuspyynto "x-iam-accesstoken") + iam-data-public-key (-> initialisoidut-tiedot :iam-data-public-key) + accesstoken-public-key (-> initialisoidut-tiedot :accesstoken-public-key)] + + + (testing "Pääsy Harjaan estetään, public key url on väärin" + (nollaa-todennuksen-cache) + (let [;; kehitysmoodi? false Laukaisee GET kutsun, + ;; mutta koska mock-https-issuer, public-key-url eivät sisällä validia linkkiä, kutsu ei mene mihinkään + ;; Pitäisi johtaa virheeseen, ja estää pääsy + kehitysmoodi? false + todennus-varmistus-paalla? true + public-key "string" + vastaus (palauta-cognito-todennuspyynto-vastaus kehitysmoodi? public-key todennus-varmistus-paalla? todennuspyynto) + odotetut-harja-roolit (:roolit odotetut-roolit) + odotetut-urakka-roolit (:urakkaroolit odotetut-roolit)] + + ;; failed tarkoittaa että käyttäjä ohjattiin "Ei käyttöoikeutta Harjaan" + (is (= (get-in vastaus [:headers "oam_groups"]) "failed") "Käyttäjältä estetään pääsy") + (is (= (get-in vastaus [:kayttaja :roolit]) #{"failed"}) "Käyttäjältä estetään pääsy") + (is (true? (atomi-sisaltaa-stringin? timbre-log-historia "Public avaimen päivitys epäonnistui: No matching clause")) "Odotettu virhe tapahtuu") + (is (true? (atomi-sisaltaa-stringin? timbre-log-historia "Authentikointi ei onnistunut: No method in multimethod")) "Odotettu virhe tapahtuu") + + ;; Roolien ei pitäisi täsmätä + (is (not= (get-in vastaus [:kayttaja :roolit]) odotetut-harja-roolit) "Käyttäjällä ei ole harja rooleja") + (is (not= (get-in vastaus [:kayttaja :urakkaroolit]) odotetut-urakka-roolit) "Käyttäjällä ei ole harja rooleja") + + ;; Muiden perustietojen pitäisi silti olla OK + (tarkista-cognito-todennus-perustiedot vastaus x-iam-accesstoken x-iam-data))) + + + (testing "Pääsy Harjaan estetään, signature ei täsmää" + (nollaa-todennuksen-cache) + (let [kehitysmoodi? true + public-key [accesstoken-public-key iam-data-public-key] + + ;; Injectaa tokeniin eri expiration date + fn-encode (fn [s] + (-> s cheshire/encode (.getBytes "UTF-8") + Base64/encodeBase64 (String. "UTF-8"))) + fake-jwt (fn [decoded] + (let [{:keys [header payload signature]} decoded] + (str (fn-encode header) "." (fn-encode payload) "." signature))) + inject_payload (-> x-iam-accesstoken (jwt-varmistus/dekoodaa-token)) + new-payload (assoc (:payload inject_payload) :exp "1798652411") + injected-token (fake-jwt (assoc inject_payload :payload new-payload)) + + ;; Tee kutsu muokatulla tokenilla, pitäisi epäonnistua, ja tulla rooliksi failed + vahvistetut-tunnustiedot (jwt-varmistus/vahvista-jwt-signaturet injected-token x-iam-data kehitysmoodi? public-key)] + + (is (= (get vahvistetut-tunnustiedot "custom:rooli") "failed") "Käyttö Harjaan estetään") + (is (true? (atomi-sisaltaa-stringin? timbre-log-historia "Authentikointi ei onnistunut: Message seems corrupt or manipulated")) "Odotettu virhe tapahtuu"))))) + + +(deftest ei-public-avainta-asetettu-paasta-kayttaja-harjaan-test + ;; Jos public avainta ei ole asetettu, käyttäjän pitäisi päästä Harjaan + ;; (koska authentikaatiota ei voi tällöin suorittaa) + (let [initialisoidut-tiedot (initialisoi-cognito-jwt-todennuspyynto) + todennuspyynto (-> initialisoidut-tiedot :todennuspyynto) + x-iam-data (get todennuspyynto "x-iam-data") + x-iam-accesstoken (get todennuspyynto "x-iam-accesstoken") + public-key nil + kehitysmoodi? false + todennus-varmistus-paalla? true + vastaus (palauta-cognito-todennuspyynto-vastaus kehitysmoodi? public-key todennus-varmistus-paalla? todennuspyynto) + odotetut-harja-roolit (:roolit odotetut-roolit) + odotetut-urakka-roolit (:urakkaroolit odotetut-roolit)] + + (is (true? (atomi-sisaltaa-stringin? timbre-log-historia "Authentikointia ei voida tehdä, public-key-url ei ole asetettu"))) + + ;; Ei pitäisi olla failed tilassa + (is (not= (get-in vastaus [:headers "oam_groups"]) "failed") "Käyttäjän pitäisi päästä Harjaan (critical)") + (is (not= (get-in vastaus [:kayttaja :roolit]) #{"failed"}) "Käyttäjän pitäisi päästä Harjaan (critical)") + + ;; Roolit pitäisi olla OK, ja käyttäjän pitäisi päästä Harjaan + (is (= (get-in vastaus [:kayttaja :roolit]) odotetut-harja-roolit) "Käyttäjän roolit saatiin (critical)") + (is (= (get-in vastaus [:kayttaja :urakkaroolit]) odotetut-urakka-roolit) "Käyttäjän urakkaroolit saatiin (critical)") + (tarkista-cognito-todennus-perustiedot vastaus x-iam-accesstoken x-iam-data))) + + +(deftest kutsutaanko-jwt-varmistus-test + (let [initialisoidut-tiedot (initialisoi-cognito-jwt-todennuspyynto) + todennuspyynto (-> initialisoidut-tiedot :todennuspyynto) + x-iam-data (get todennuspyynto "x-iam-data") + x-iam-accesstoken (get todennuspyynto "x-iam-accesstoken")] + + (testing "Varmistusta ei tehdä, koska varmistus poissa päältä" + (nollaa-todennuksen-cache) + (let [public-key "olemassa" + kehitysmoodi? false + todennus-varmistus-paalla? false + vastaus (palauta-cognito-todennuspyynto-vastaus kehitysmoodi? public-key todennus-varmistus-paalla? todennuspyynto) + odotetut-harja-roolit (:roolit odotetut-roolit) + odotetut-urakka-roolit (:urakkaroolit odotetut-roolit)] + + (is (false? (atomi-sisaltaa-stringin? timbre-log-historia "Todennettiin onnistuneesti")) "Todennusta ei tehdä") + + ;; Ei pitäisi olla failed tilassa + (is (not= (get-in vastaus [:headers "oam_groups"]) "failed") "Käyttäjän pitäisi päästä Harjaan (critical)") + (is (not= (get-in vastaus [:kayttaja :roolit]) #{"failed"}) "Käyttäjän pitäisi päästä Harjaan (critical)") + + ;; Roolit pitäisi olla OK, ja käyttäjän pitäisi päästä Harjaan + (is (= (get-in vastaus [:kayttaja :roolit]) odotetut-harja-roolit) "Käyttäjän roolit saatiin (critical)") + (is (= (get-in vastaus [:kayttaja :urakkaroolit]) odotetut-urakka-roolit) "Käyttäjän urakkaroolit saatiin (critical)") + (tarkista-cognito-todennus-perustiedot vastaus x-iam-accesstoken x-iam-data))))) + + +(deftest varmista-authentikoinnin-toiminnallisuus + (let [initialisoidut-tiedot (initialisoi-cognito-jwt-todennuspyynto) + todennuspyynto (-> initialisoidut-tiedot :todennuspyynto) + iam-data-public-key (-> initialisoidut-tiedot :iam-data-public-key) + accesstoken-public-key (-> initialisoidut-tiedot :accesstoken-public-key) + x-iam-data (get todennuspyynto "x-iam-data") + x-iam-accesstoken (get todennuspyynto "x-iam-accesstoken") + tunnistetiedot-muunnettuna (jwt-varmistus/tunnistetiedot x-iam-data)] + + + (testing "Authentikaation cache asetettu" + (is (> jwt-varmistus/+public-key-cache-paivitys-min+ 0)) + (is (> jwt-varmistus/+kayttaja-varmistus-cache-min+ 0))) + + + (testing "x-iam-data public key PEM parsinta toimii oikein (CRITICAL)" + (let [ec-key-pair (generate-ec-key-pair) + ec-public-key (.getPublic ec-key-pair) + ec-public-key-PEM (public-key-to-pem ec-public-key) + ec-public-key-PEM-converted (jwt-varmistus/parsi-PEM-public-key ec-public-key-PEM)] + (is (= ec-public-key ec-public-key-PEM-converted)))) + + + (testing "Public avainten päivitys toimii, GET kutsu tehdään (CRITICAL)" + (nollaa-todennuksen-cache) + (let [PEM? true + paivita? true + kehitysmoodi? true + avaimen-tunniste (gensym) + paivitys-intervalli-min 15 + menneisyydessa-minuuttia 200 + ajan-tasalla (atom {:key avaimen-tunniste :fetched-at (System/currentTimeMillis)}) + vanhentunut (atom {:key avaimen-tunniste :fetched-at (- (System/currentTimeMillis) (* menneisyydessa-minuuttia 60 1000))}) + haettu-ennen (-> @vanhentunut :fetched-at) + + ;; GET kutsu ei mene mihinkään ulos, käytetään mock linkkiä, mutta simuloidaan sitä, että se kuitenkin tehdään + _ (jwt-varmistus/hae-public-key paivita? mock-uid mock-https-issuer PEM? vanhentunut paivitys-intervalli-min kehitysmoodi?) + haettu-jalkeen (-> @vanhentunut :fetched-at)] + + ;; Kumpikaan ei pitäisi olla vanhentunut, koska vanhentunut päivitettiin + (is (false? (jwt-varmistus/public-key-vanhentunut? vanhentunut jwt-varmistus/+public-key-cache-paivitys-min+))) + (is (false? (jwt-varmistus/public-key-vanhentunut? ajan-tasalla jwt-varmistus/+public-key-cache-paivitys-min+))) + + ;; Kutsu tehtiin ja timestamp päivitettiin + (is (not= haettu-ennen haettu-jalkeen) "Timestamp ei täsmää") + (is (> haettu-jalkeen haettu-ennen) "GET kutsu tehtiin ja atom päivitettiin"))) + + + (testing "Public avain invlid link virhe test" + (nollaa-todennuksen-cache) + (let [PEM? true + paivita? true + kehitysmoodi? false + avaimen-tunniste (gensym) + paivitys-intervalli-min 15 + menneisyydessa-minuuttia 200 + ajan-tasalla (atom {:key avaimen-tunniste :fetched-at (System/currentTimeMillis)}) + vanhentunut (atom {:key avaimen-tunniste :fetched-at (- (System/currentTimeMillis) (* menneisyydessa-minuuttia 60 1000))}) + haettu-ennen (-> @vanhentunut :fetched-at) + + ;; Koska pem avainta ei ole, on odotettavissa että kutsu epäonnistuu, mutta yritettiin kuitenkin tehdä + _ (jwt-varmistus/hae-public-key paivita? mock-uid mock-https-issuer PEM? vanhentunut paivitys-intervalli-min kehitysmoodi?) + haettu-jalkeen (-> @vanhentunut :fetched-at)] + + ;; Tarkista että cachen päivitysfunktio toimii, vanhentunut pitäisi palauttaa true + (is (true? (jwt-varmistus/public-key-vanhentunut? vanhentunut jwt-varmistus/+public-key-cache-paivitys-min+))) + ;; Ajantasalla oleva pitäisi palauttaa false + (is (false? (jwt-varmistus/public-key-vanhentunut? ajan-tasalla jwt-varmistus/+public-key-cache-paivitys-min+))) + + ;; Fetched pitäisi olla saman arvoinen luku tässä tapauksessa + (is (= haettu-ennen haettu-jalkeen)) + ;; Vahvista että virhe tapahtui + (is (true? (atomi-sisaltaa-stringin? timbre-log-historia "Public avaimen päivitys epäonnistui"))))) + + + (testing "Tokenin dekoodaus toimii (CRITICAL)" + (let [dekoodattu-iam-data (jwt-varmistus/dekoodaa-token x-iam-data) + iam-data-header (:header dekoodattu-iam-data) + iam-data-payload (:payload dekoodattu-iam-data)] + + (is (= iam-data-header (first test-iam-data-jwt))) + + (is (= (get iam-data-payload :custom:rooli) mock-roolit)) + (is (= (get iam-data-payload :custom:sukunimi) mock-sukunimi)) + (is (= (get iam-data-payload :username) mock-username)) + (is (= (get iam-data-payload :iss) mock-https-issuer)) + (is (= (get iam-data-payload :custom:ytunnus) mock-ytunnus)) + (is (= (get iam-data-payload :email) mock-email)) + (is (= (get iam-data-payload :exp) mock-expiration)) + (is (= (get iam-data-payload :custom:uid) mock-uid)) + (is (= (get iam-data-payload :custom:puhelin) mock-puh)) + (is (= (get iam-data-payload :custom:organisaatio) mock-organisaatio)) + (is (= (get iam-data-payload :custom:etunimi) mock-etunimi)) + (is (= (get iam-data-payload :email_verified) "false")) + + (is (some? (:signature dekoodattu-iam-data))))) + + + (testing "Authentikaation tunnistetiedot palauttaa käyttäjätiedot oikein (CRITICAL)" + (is (= (get tunnistetiedot-muunnettuna "custom:rooli") mock-roolit)) + (is (= (get tunnistetiedot-muunnettuna "custom:sukunimi") mock-sukunimi)) + (is (= (get tunnistetiedot-muunnettuna "username") mock-username)) + (is (= (get tunnistetiedot-muunnettuna "iss") mock-https-issuer)) + (is (= (get tunnistetiedot-muunnettuna "custom:ytunnus") mock-ytunnus)) + (is (= (get tunnistetiedot-muunnettuna "email") mock-email)) + (is (= (get tunnistetiedot-muunnettuna "exp") mock-expiration)) + (is (= (get tunnistetiedot-muunnettuna "custom:uid") mock-uid)) + (is (= (get tunnistetiedot-muunnettuna "custom:puhelin") mock-puh)) + (is (= (get tunnistetiedot-muunnettuna "custom:organisaatio") mock-organisaatio)) + (is (= (get tunnistetiedot-muunnettuna "custom:etunimi") mock-etunimi)) + (is (= (get tunnistetiedot-muunnettuna "email_verified") "false"))) + + + (testing "Pääsyä ei estetä, public avain puuttuu (suora kutsu) (CRITICAL)" + (nollaa-todennuksen-cache) + (let [kehitysmoodi? false + public-key-url nil + vahvistetut-tunnustiedot (jwt-varmistus/vahvista-jwt-signaturet x-iam-accesstoken x-iam-data kehitysmoodi? public-key-url)] + (is (= (get vahvistetut-tunnustiedot "custom:rooli") mock-roolit)) + (is (true? (atomi-sisaltaa-stringin? timbre-log-historia "Authentikointia ei voida tehdä"))))) + + + (testing "Authentikointi toimii (suora kutsu) (CRITICAL)" + (nollaa-todennuksen-cache) + (let [kehitysmoodi? true + public-key [accesstoken-public-key iam-data-public-key] + vahvistetut-tunnustiedot (jwt-varmistus/vahvista-jwt-signaturet x-iam-accesstoken x-iam-data kehitysmoodi? public-key)] + (is (= (get vahvistetut-tunnustiedot "custom:rooli") mock-roolit)) + (is (true? (atomi-sisaltaa-stringin? timbre-log-historia "Todennettiin onnistuneesti"))))) + + + ;; TODO , blokkaus ei ole vielä käytössä + ;; Enabloi vasta sitten kun on + #_(testing "Authentikointi estää pääsyn (suora kutsu), accesstoken puuttuu (CRITICAL)" + (nollaa-todennuksen-cache) + (let [kehitysmoodi? true + public-key [accesstoken-public-key iam-data-public-key] + vahvistetut-tunnustiedot (jwt-varmistus/vahvista-jwt-signaturet nil x-iam-data kehitysmoodi? public-key)] + (is (= (get vahvistetut-tunnustiedot "custom:rooli") "failed")) + (is (true? (atomi-sisaltaa-stringin? timbre-log-historia "Authentikointi ei onnistunut: JWT Token puuttui kokonaan"))))) + + + #_(testing "Authentikointi estää pääsyn (suora kutsu), iam-data puuttuu (CRITICAL)" + (nollaa-todennuksen-cache) + (let [kehitysmoodi? true + public-key [accesstoken-public-key iam-data-public-key] + vahvistetut-tunnustiedot (jwt-varmistus/vahvista-jwt-signaturet x-iam-accesstoken nil kehitysmoodi? public-key)] + (is (= (get vahvistetut-tunnustiedot "custom:rooli") "failed")) + (is (true? (atomi-sisaltaa-stringin? timbre-log-historia "Authentikointi ei onnistunut: JWT Token puuttui kokonaan"))))))) diff --git a/test/clj/harja/palvelin/komponentit/todennus_test.clj b/test/clj/harja/palvelin/komponentit/todennus_test.clj index 190efa4165..e3b52fe7b0 100644 --- a/test/clj/harja/palvelin/komponentit/todennus_test.clj +++ b/test/clj/harja/palvelin/komponentit/todennus_test.clj @@ -4,6 +4,7 @@ [harja.palvelin.komponentit.todennus :as todennus] [harja.domain.oikeudet :as oikeudet] [harja.testi :refer :all] + [harja.palvelin.komponentit.http-palvelin :as http-palvelin] [clojure.data.json :as json] [clojure.test :as t :refer [deftest is use-fixtures testing]] [com.stuartsierra.component :as component] @@ -18,7 +19,7 @@ (component/system-map :db (tietokanta/luo-tietokanta testitietokanta) :todennus (component/using - (todennus/http-todennus nil) + (todennus/http-todennus) [:db]))))) (testit) (alter-var-root #'jarjestelma component/stop)) @@ -132,13 +133,15 @@ Base64/encodeBase64 (String. "UTF-8")) [(first x-iam-data) (second x-iam-data)]) jwt (str (str/join "." jwt) "." (nth x-iam-data 2))] - {"x-iam-data" jwt})) + {;; Vaaditaan että molemmat tokenit on aina Cognito kutsussa mukana + "x-iam-data" jwt + "x-iam-accesstoken" jwt})) (deftest cognito-headereiden-purku-oam-ja-entraid (let [handler (-> (fn [req] req) - (harja.palvelin.komponentit.http-palvelin/wrap-with-common-wrappers)) - todenna #(todennus/todenna-pyynto (:todennus jarjestelma) %) + (http-palvelin/wrap-with-common-wrappers true)) + todenna #(todennus/todenna-pyynto (:todennus jarjestelma) % true) destia-id (first (first (q "SELECT id FROM organisaatio WHERE nimi = 'Destia Oy'"))) virasto-id (first (first (q "SELECT id FROM organisaatio WHERE nimi = 'Liikennevirasto'")))] @@ -185,8 +188,8 @@ (deftest cognito-headereiden-purku-harja-api-usernamella (let [handler (-> (fn [req] req) - (harja.palvelin.komponentit.http-palvelin/wrap-with-common-wrappers)) - todenna #(todennus/todenna-pyynto (:todennus jarjestelma) %)] + (http-palvelin/wrap-with-common-wrappers)) + todenna #(todennus/todenna-pyynto (:todennus jarjestelma) % true)] (testing "Cognito headeri: harja-api-username -headerin arvo löytyy custom:uid-headerin arvon sijaan" (let [req (handler {:headers (merge (testi-enkoodaa-payload-jwt testi-cognito-headerit-oam) {"harja-api-username" "LOTTA"}) }) @@ -207,7 +210,7 @@ (is (= (get-in req [:kayttaja :sukunimi]) "Destialainen")))))) (deftest ota-organisaatio-roolin-y-tunnuksesta - (let [todenna #(todennus/todenna-pyynto (:todennus jarjestelma) %) + (let [todenna #(todennus/todenna-pyynto (:todennus jarjestelma) % true) destia-id (first (first (q "SELECT id FROM organisaatio WHERE nimi = 'Destia Oy'"))) lampunvaihtajat-id (first (first (q "SELECT id FROM organisaatio WHERE ytunnus = '2234567-8'")))] (testing "Organisaatio löytyy, jos OAM_ORGANIZATION on annettu oikein" @@ -228,11 +231,11 @@ "oam_user_mail" "alpo@example.com" "oam_user_mobile" "1234567890" "oam_organization" "Eitällaistaolekaan Oy" - "oam_groups" "2234567-8_Paakayttaja"}})] + "oam_groups" "2234567-8_Paakayttaja"}} true)] (is (= (get-in req [:kayttaja :organisaatio :id]) lampunvaihtajat-id)))))) (deftest ota-organisaatio-companyid-headerista - (let [todenna #(todennus/todenna-pyynto (:todennus jarjestelma) %) + (let [todenna #(todennus/todenna-pyynto (:todennus jarjestelma) % true) destia-id (first (first (q "SELECT id FROM organisaatio WHERE nimi = 'Destia Oy'")))] (testing "Organisaatio löytyy, jos OAM_USER_COMPANYID on annettu oikein vaikka nimi olisi väärä" (let [req (todenna {:headers {"oam_remote_user" "daniel" @@ -254,5 +257,5 @@ "oam_user_mobile" "1234567890" "oam_organization" "Destia oy" "oam_user_companyid" "NOT_FOUND" - "oam_groups" ""}})] + "oam_groups" ""}} true)] (is (= (get-in req [:kayttaja :organisaatio :id]) destia-id)))))) diff --git a/test/clj/harja/palvelin/palvelut/pois_kytketyt_ominaisuudet_test.clj b/test/clj/harja/palvelin/palvelut/pois_kytketyt_ominaisuudet_test.clj index bb28b1f0fd..c66825fa56 100644 --- a/test/clj/harja/palvelin/palvelut/pois_kytketyt_ominaisuudet_test.clj +++ b/test/clj/harja/palvelin/palvelut/pois_kytketyt_ominaisuudet_test.clj @@ -1,9 +1,7 @@ (ns harja.palvelin.palvelut.pois-kytketyt-ominaisuudet-test - (:require [harja.palvelin.komponentit.todennus :as todennus] - [harja.testi :refer :all] + (:require [harja.testi :refer :all] [harja.palvelin.asetukset :refer [ominaisuus-kaytossa? pois-kytketyt-ominaisuudet]] - [clojure.test :as t :refer [deftest is use-fixtures testing]] - [com.stuartsierra.component :as component])) + [clojure.test :as t :refer [is]])) (t/deftest ominaisuudet-pois-paalta-ennen-asetusten-lukua (let [alkup-pko @pois-kytketyt-ominaisuudet] diff --git a/test/clj/harja/testi.clj b/test/clj/harja/testi.clj index 9443fb595d..94c241d134 100644 --- a/test/clj/harja/testi.clj +++ b/test/clj/harja/testi.clj @@ -592,6 +592,12 @@ urakka-id (throw (Exception. (format "Annetulla nimellä: '%s' ei löydy urakkaa!!" nimi)))))) +(defn hae-urakan-id-sampo-idlla [sampoid] + (ffirst (q (str "SELECT id FROM urakka WHERE sampoid = '" sampoid "';")))) + +(defn hae-urakoitsijan-id-ytunnuksella [ytunnus] + (ffirst (q (str "SELECT id FROM organisaatio WHERE tyyppi = 'urakoitsija' AND ytunnus = '" ytunnus "';")))) + (defn kutsu-http-palvelua "Lyhyt muoto testijärjestelmän HTTP palveluiden kutsumiseen." ([nimi kayttaja]