From 091c86db4255b9afe4eb69f5279780ba8132531e Mon Sep 17 00:00:00 2001 From: Aleksi Ahtiainen Date: Thu, 31 Oct 2024 13:55:27 +0200 Subject: [PATCH 1/6] =?UTF-8?q?Uudelleennime=C3=A4=20loogisemmin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/{oauth-client-config.ts => oauth2-client-config.ts} | 0 omadata-oauth2-sample/server/src/oauth2-client/oauth2-client.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename omadata-oauth2-sample/server/src/config/{oauth-client-config.ts => oauth2-client-config.ts} (100%) diff --git a/omadata-oauth2-sample/server/src/config/oauth-client-config.ts b/omadata-oauth2-sample/server/src/config/oauth2-client-config.ts similarity index 100% rename from omadata-oauth2-sample/server/src/config/oauth-client-config.ts rename to omadata-oauth2-sample/server/src/config/oauth2-client-config.ts diff --git a/omadata-oauth2-sample/server/src/oauth2-client/oauth2-client.ts b/omadata-oauth2-sample/server/src/oauth2-client/oauth2-client.ts index 70b9a70306..2b53436d59 100644 --- a/omadata-oauth2-sample/server/src/oauth2-client/oauth2-client.ts +++ b/omadata-oauth2-sample/server/src/oauth2-client/oauth2-client.ts @@ -1,4 +1,4 @@ -import { getOAuthClientConfig } from '../config/oauth-client-config.js' +import { getOAuthClientConfig } from '../config/oauth2-client-config.js' import * as client from 'openid-client' import { resourceEndpointUrl } from '../config/koski-backend-config.js' import { Request } from 'express' From 160bf3d8904001f7b68b9bc0dab4d607c160d9a7 Mon Sep 17 00:00:00 2001 From: Aleksi Ahtiainen Date: Thu, 31 Oct 2024 13:57:15 +0200 Subject: [PATCH 2/6] =?UTF-8?q?Lis=C3=A4=C3=A4=20=5F:t=20scope-koodiarvoih?= =?UTF-8?q?in;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ilman niitä ei pysty tekemään mäppäystä koodiarvoista käyttöoikeuksiin, joissa alaviivat esiintyvät. Alaviivojen kanssa merkkijonot ovat myös helpommin luettavat. --- .../koodisto/koodit/omadataoauth2scope.json | 14 +++++++------- web/app/omadata/OmaDataOAuth2AnnaHyvaksynta.jsx | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/resources/mockdata/koodisto/koodit/omadataoauth2scope.json b/src/main/resources/mockdata/koodisto/koodit/omadataoauth2scope.json index 329290ea0b..7c388420bd 100644 --- a/src/main/resources/mockdata/koodisto/koodit/omadataoauth2scope.json +++ b/src/main/resources/mockdata/koodisto/koodit/omadataoauth2scope.json @@ -25,7 +25,7 @@ "levelsWithCodeElements": [], "koodiUri": "omadataoauth2scope_henkilotiedotnimi", "versio": 1, - "koodiArvo": "henkilotiedotnimi" + "koodiArvo": "henkilotiedot_nimi" }, { "metadata": [ @@ -53,7 +53,7 @@ "levelsWithCodeElements": [], "koodiUri": "omadataoauth2scope_henkilotiedotsyntymaaika", "versio": 1, - "koodiArvo": "henkilotiedotsyntymaaika" + "koodiArvo": "henkilotiedot_syntymaaika" }, { "metadata": [ @@ -81,7 +81,7 @@ "levelsWithCodeElements": [], "koodiUri": "omadataoauth2scope_henkilotiedothetu", "versio": 1, - "koodiArvo": "henkilotiedothetu" + "koodiArvo": "henkilotiedot_hetu" }, { "metadata": [ @@ -109,7 +109,7 @@ "levelsWithCodeElements": [], "koodiUri": "omadataoauth2scope_opiskeluoikeudetsuoritetuttutkinnot", "versio": 1, - "koodiArvo": "opiskeluoikeudetsuoritetuttutkinnot" + "koodiArvo": "opiskeluoikeudet_suoritetut_tutkinnot" }, { "metadata": [ @@ -137,12 +137,12 @@ "levelsWithCodeElements": [], "koodiUri": "omadataoauth2scope_opiskeluoikeudetaktiivisetjapaattyneetopinnot", "versio": 1, - "koodiArvo": "opiskeluoikeudetaktiivisetjapaattyneetopinnot" + "koodiArvo": "opiskeluoikeudet_aktiiviset_ja_paattyneet_opinnot" }, { "metadata": [ { - "nimi": "Kaikki tiedot", + "nimi": "Kaikki opiskeluoikeustiedot", "kuvaus": "", "lyhytNimi": "", "kieli": "FI" @@ -165,6 +165,6 @@ "levelsWithCodeElements": [], "koodiUri": "omadataoauth2scope_opiskeluoikeudetkaikkitiedot", "versio": 1, - "koodiArvo": "opiskeluoikeudetkaikkitiedot" + "koodiArvo": "opiskeluoikeudet_kaikki_tiedot" } ] diff --git a/web/app/omadata/OmaDataOAuth2AnnaHyvaksynta.jsx b/web/app/omadata/OmaDataOAuth2AnnaHyvaksynta.jsx index 756988f7e0..73c5825ad1 100644 --- a/web/app/omadata/OmaDataOAuth2AnnaHyvaksynta.jsx +++ b/web/app/omadata/OmaDataOAuth2AnnaHyvaksynta.jsx @@ -44,7 +44,7 @@ const ScopeList = ({scope}) => { return koodi } const koodistoRecord = scopesKoodisto.find( - (k) => k.koodiviite.koodiarvo === koodi.toLowerCase().replaceAll('_', '') + (k) => k.koodiviite.koodiarvo === koodi.toLowerCase() ) return koodistoRecord ? t(koodistoRecord.koodiviite.nimi) : koodi } From bc64ba8c188a56c4f13a08a294dbc49343ccda27 Mon Sep 17 00:00:00 2001 From: Aleksi Ahtiainen Date: Thu, 31 Oct 2024 13:58:25 +0200 Subject: [PATCH 3/6] =?UTF-8?q?Lis=C3=A4=C3=A4=20scope-koodistoon=20koodia?= =?UTF-8?q?rvo,=20joka=20k=C3=A4ytt=C3=B6oikeuspalvelussa=20on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koodisto/koodit/omadataoauth2scope.json | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/main/resources/mockdata/koodisto/koodit/omadataoauth2scope.json b/src/main/resources/mockdata/koodisto/koodit/omadataoauth2scope.json index 7c388420bd..bf97fa4e69 100644 --- a/src/main/resources/mockdata/koodisto/koodit/omadataoauth2scope.json +++ b/src/main/resources/mockdata/koodisto/koodit/omadataoauth2scope.json @@ -83,6 +83,34 @@ "versio": 1, "koodiArvo": "henkilotiedot_hetu" }, + { + "metadata": [ + { + "nimi": "Kaikki henkilötiedot", + "kuvaus": "", + "lyhytNimi": "", + "kieli": "FI" + }, + { + "nimi": "All information", + "kuvaus": "", + "lyhytNimi": "", + "kieli": "SV" + }, + { + "nimi": "All information", + "kuvaus": "", + "lyhytNimi": "", + "kieli": "EN" + } + ], + "withinCodeElements": [], + "includesCodeElements": [], + "levelsWithCodeElements": [], + "koodiUri": "omadataoauth2scope_henkilotiedotkaikkitiedot", + "versio": 1, + "koodiArvo": "henkilotiedot_kaikki_tiedot" + }, { "metadata": [ { From 45c15f9048efeb09914c35f142ff7648f0ccfb46 Mon Sep 17 00:00:00 2001 From: Aleksi Ahtiainen Date: Thu, 31 Oct 2024 13:59:15 +0200 Subject: [PATCH 4/6] =?UTF-8?q?Lis=C3=A4=C3=A4=20metadata-route,=20josta?= =?UTF-8?q?=20OAuth2-palvelun=20tiedot=20saa=20haettua;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ota tämä käyttöön myös esimerkki-applikaatiossa. --- omadata-oauth2-sample/client/package.json | 2 +- .../server/src/config/oauth2-client-config.ts | 46 +++--------- src/main/resources/reference.conf | 1 + src/main/scala/ScalatraBootstrap.scala | 8 +- .../OmaDataOAuth2DiscoveryServlet.scala | 73 +++++++++++++++++++ 5 files changed, 94 insertions(+), 36 deletions(-) create mode 100644 src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2DiscoveryServlet.scala diff --git a/omadata-oauth2-sample/client/package.json b/omadata-oauth2-sample/client/package.json index 3468926c93..2eb07bbc18 100644 --- a/omadata-oauth2-sample/client/package.json +++ b/omadata-oauth2-sample/client/package.json @@ -32,7 +32,7 @@ "start-react-scripts": "react-scripts start", "start-with-server": "npm-run-all --parallel start-server start", "start-with-server-and-luovutuspalvelu": "npm-run-all --parallel build-and-start-luovutuspalvelu start-server start", - "start-server": "NODE_EXTRA_CA_CERTS=../../koski-luovutuspalvelu/proxy/test/testca/certs/root-ca.crt CLIENT_ID=omadataoauth2sample ENABLE_LOCAL_MTLS=true TOKEN_ENDPOINT_URL=https://localhost:7022/koski/api/omadata-oauth2/authorization-server RESOURCE_ENDPOINT_URL=https://localhost:7022/koski/api/omadata-oauth2/resource-server npm run --prefix ../server start", + "start-server": "NODE_EXTRA_CA_CERTS=../../koski-luovutuspalvelu/proxy/test/testca/certs/root-ca.crt CLIENT_ID=omadataoauth2sample ENABLE_LOCAL_MTLS=true RESOURCE_ENDPOINT_URL=https://localhost:7022/koski/api/omadata-oauth2/resource-server npm run --prefix ../server start", "build-and-start-luovutuspalvelu": "KOSKI_BACKEND_HOST=${KOSKI_BACKEND_HOST:-http://$(node scripts/getmyip.js):7021} CLIENT_USERNAME='omadataoauth2sample' CLIENT_PASSWORD='omadataoauth2sample' npm run --prefix ../../koski-luovutuspalvelu/proxy local", "build": "react-scripts build", "test": "react-scripts test", diff --git a/omadata-oauth2-sample/server/src/config/oauth2-client-config.ts b/omadata-oauth2-sample/server/src/config/oauth2-client-config.ts index 2250ece90f..e0fe813371 100644 --- a/omadata-oauth2-sample/server/src/config/oauth2-client-config.ts +++ b/omadata-oauth2-sample/server/src/config/oauth2-client-config.ts @@ -5,49 +5,27 @@ import { enableLocalMTLS, getClientCertSecret } from './client-cert-config.js' import * as undici from 'undici' import { koskiBackendHost } from './koski-backend-config.js' -const authorizationEndpointUrl = `${koskiBackendHost}/koski/omadata-oauth2/authorize` -const tokenEndpointUrl = - process.env.TOKEN_ENDPOINT_URL || - `${koskiBackendHost}/koski/api/omadata-oauth2/authorization-server` const clientId = process.env.CLIENT_ID || 'oauth2client' -const serverMetadata: client.ServerMetadata = { - issuer: 'KOSKI', - - authorization_endpoint: authorizationEndpointUrl, - response_types_supported: ['code'], - response_modes_supported: ['form_post'], - scopes_supported: [ - 'HENKILOTIEDOT_NIMI', - 'HENKILOTIEDOT_SYNTYMAAIKA', - 'HENKILOTIEDOT_HETU', - 'HENKILOTIEDOT_KAIKKI_TIEDOT', - 'OPISKELUOIKEUDET_SUORITETUT_TUTKINNOT', - 'OPISKELUOIKEUDET_AKTIIVISET_JA_PAATTYNEET_OPINNOT', - 'OPISKELUOIKEUDET_KAIKKI_TIEDOT' - ], - code_challenge_methods_supported: ['S256'], - - token_endpoint: tokenEndpointUrl, - token_endpoint_auth_methods_supported: ['tls_client_auth'], - token_endpoint_auth_signing_alg_values_supported: undefined, - grant_types_supported: ['authorization_code'], - - tls_client_certificate_bound_access_tokens: false, - - service_documentation: - 'https://github.com/Opetushallitus/koski/blob/master/documentation/oauth2toteutus.md' -} const clientMetadata: client.ClientMetadata = { client_id: clientId, use_mtls_endpoint_aliases: false } export const getOAuthClientConfig = memoize( async (): Promise => { - let config = new client.Configuration( - serverMetadata, + const discoveryOptions = enableLocalMTLS + ? { + execute: [client.allowInsecureRequests] + } + : undefined + + let config = await client.discovery( + new URL( + `${koskiBackendHost}/koski/omadata-oauth2/.well-known/oauth-authorization-server` + ), clientId, clientMetadata, - client.TlsClientAuth() + client.TlsClientAuth(), + discoveryOptions ) const certs = await getClientCertSecret() diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 9123553ac6..82916948f1 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -217,6 +217,7 @@ mydata = { } omadataoauth2 = { + luovutuspalveluBaseUrl = "mock" login { cas { fi = "/koski/login/oppija" # Login: "Korhopankki", or Tupas in production diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index ccf186ad58..95de710ce4 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -20,7 +20,7 @@ import fi.oph.koski.log.Logging import fi.oph.koski.luovutuspalvelu.{PalveluvaylaServlet, TilastokeskusServlet} import fi.oph.koski.migri.MigriServlet import fi.oph.koski.mydata.{ApiProxyServlet, MyDataReactServlet, MyDataServlet} -import fi.oph.koski.omadataoauth2.{OmaDataOAuth2AuthorizationServerServlet, OmaDataOAuth2CASWorkaroundServlet, OmaDataOAuth2LogoutPostResponseServlet, OmaDataOAuth2PostResponseDebugServlet, OmaDataOAuth2ResourceOwnerReactServlet, OmaDataOAuth2ResourceOwnerServlet, OmaDataOAuth2ResourceServerServlet} +import fi.oph.koski.omadataoauth2.{OmaDataOAuth2AuthorizationServerServlet, OmaDataOAuth2CASWorkaroundServlet, OmaDataOAuth2DiscoveryServlet, OmaDataOAuth2LogoutPostResponseServlet, OmaDataOAuth2PostResponseDebugServlet, OmaDataOAuth2ResourceOwnerReactServlet, OmaDataOAuth2ResourceOwnerServlet, OmaDataOAuth2ResourceServerServlet} import fi.oph.koski.omaopintopolkuloki.OmaOpintoPolkuLokiServlet import fi.oph.koski.omattiedot.{OmatTiedotHtmlServlet, OmatTiedotServlet, OmatTiedotServletV2} import fi.oph.koski.opiskeluoikeus.{OpiskeluoikeusServlet, OpiskeluoikeusValidationServlet} @@ -185,6 +185,12 @@ class ScalatraBootstrap extends LifeCycle with Logging with Timing with GlobalEx mount("/koski/api/omadata-oauth2/authorization-server", new OmaDataOAuth2AuthorizationServerServlet) mount("/koski/api/omadata-oauth2/resource-server", new OmaDataOAuth2ResourceServerServlet) + // TODO: TOR-2210: Speksin https://www.rfc-editor.org/rfc/rfc8414 mukaan tämä pitäisi oikeasti olla routessa + // https://opintopolku.fi/.well-known/oauth-authorization-server/koski/omadata-oauth2 , mutta se vaatisi + // OPH:n nginx:äänkin uusia routeja. Siksi toistaiseksi väärin muodostetussa polussa + // https://opintopolku.fi/koski/omadata-oauth2/.well-known/oauth-authorization-server . + mount("/koski/omadata-oauth2/.well-known/oauth-authorization-server", new OmaDataOAuth2DiscoveryServlet) + // TODO: TOR-2210: Poista debug-servlet kokonaan! mount("/koski/omadata-oauth2/debug-post-response", new OmaDataOAuth2PostResponseDebugServlet) } diff --git a/src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2DiscoveryServlet.scala b/src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2DiscoveryServlet.scala new file mode 100644 index 0000000000..5da174b42e --- /dev/null +++ b/src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2DiscoveryServlet.scala @@ -0,0 +1,73 @@ +package fi.oph.koski.omadataoauth2 + +import fi.oph.koski.config.{Environment, KoskiApplication} +import fi.oph.koski.frontendvalvonta.FrontendValvontaMode +import fi.oph.koski.koodisto.{KoodistoKoodi, KoodistoViite} +import fi.oph.koski.koskiuser.{Unauthenticated} +import fi.oph.koski.log.Logging +import fi.oph.koski.servlet.{KoskiSpecificApiServlet, NoCache} +import org.scalatra.{ContentEncodingSupport} + +class OmaDataOAuth2DiscoveryServlet(implicit val application: KoskiApplication) extends KoskiSpecificApiServlet + with Logging with ContentEncodingSupport with NoCache with Unauthenticated { + + val allowFrameAncestors: Boolean = !Environment.isServerEnvironment(application.config) + val frontendValvontaMode: FrontendValvontaMode.FrontendValvontaMode = + FrontendValvontaMode(application.config.getString("frontend-valvonta.mode")) + + get("/") { + + val opintopolkuBaseUrl = application.config.getString("opintopolku.oppija.url") match { + case "mock" => + val url = request.getRequestURL + val uri = request.getRequestURI + url.substring(0, url.indexOf(uri)) + case url => url + } + val luovutuspalveluBaseUrl = application.config.getString("omadataoauth2.luovutuspalveluBaseUrl") match { + case "mock" => + "https://localhost:7022" + case url => url + } + + val metadata = OAuth2ProviderMetadata( + issuer = opintopolkuBaseUrl + "/koski/omadata-oauth2", + authorization_endpoint = opintopolkuBaseUrl + "/koski/omadata-oauth2/authorize", + scopes_supported = findKoodisto("omadataoauth2scope").map(_.koodiArvo.toUpperCase()).sorted, + token_endpoint = luovutuspalveluBaseUrl + "/koski/api/omadata-oauth2/authorization-server", + service_documentation = opintopolkuBaseUrl + "/koski/dokumentaatio/rajapinnat/omadata-oauth2" + ) + + render(metadata) + } + + def findKoodisto(koodistoUri: String, versioNumero: Option[String] = None): Seq[KoodistoKoodi] = { + val versio: Option[KoodistoViite] = versioNumero match { + case Some("latest") => + application.koodistoPalvelu.getLatestVersionOptional(koodistoUri) + case Some(versio) => + Some(KoodistoViite(koodistoUri, versio.toInt)) + case _ => + application.koodistoPalvelu.getLatestVersionOptional(koodistoUri) + } + versio.toSeq.flatMap { koodisto => (application.koodistoPalvelu.getKoodistoKoodit(koodisto)) } + } +} + +case class OAuth2ProviderMetadata( + issuer: String, + + authorization_endpoint: String, + response_types_supported: Seq[String] = Seq("code"), + response_modes_supported: Seq[String] = Seq("form_post"), + scopes_supported: Seq[String], + code_challenge_methods_supported: Seq[String] = Seq("S256"), + + token_endpoint: String, + token_endpoint_auth_methods_supported: Seq[String] = Seq("tls_client_auth"), + grant_types_supported: Seq[String] = Seq("authorization_code"), + + tls_client_certificate_bound_access_tokens: Boolean = false, + + service_documentation: String +) From 9aa630bd6b0a6a7a05ff113de2c87671208290c1 Mon Sep 17 00:00:00 2001 From: Aleksi Ahtiainen Date: Thu, 31 Oct 2024 15:25:24 +0200 Subject: [PATCH 5/6] =?UTF-8?q?Lis=C3=A4=C3=A4=20ensimm=C3=A4inen=20versio?= =?UTF-8?q?=20dokumentaatiosivusta=20OAuth2-rajapinnalle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/documentation/omadata_oauth2.md | 173 ++++++++++++++++++ .../DocumentationApiServlet.scala | 3 +- .../documentation/DocumentationServlet.scala | 2 +- .../OmaDataOAuth2DiscoveryServlet.scala | 2 +- .../OmaDataOAuth2Documentation.scala | 55 ++++++ web/app/dokumentaatio/Dokumentaatio.jsx | 37 ++++ web/app/virkailija/virkailijaRouter.jsx | 4 +- 7 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/documentation/omadata_oauth2.md create mode 100644 src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2Documentation.scala diff --git a/src/main/resources/documentation/omadata_oauth2.md b/src/main/resources/documentation/omadata_oauth2.md new file mode 100644 index 0000000000..bbca633e56 --- /dev/null +++ b/src/main/resources/documentation/omadata_oauth2.md @@ -0,0 +1,173 @@ +# OAuth2 Omadata-rajapinta + +Tällä sivulla kuvataan rajapinnat, joilla kolmannet osapuolet (kumppanit) voivat pyytää +kansalaiselta käyttölupaa kansalaisen tietoihin ja hakea tietoja OAuth2-standardirajapinnan kautta. + +Kumppanien tulee olla Opetushallituksen hyväksymä, ja kumppanin tekniset tunnisteet pitää olla +lisättyinä KOSKI-järjestelmään. Lisäämistä varten OPH:ssa tarvittavat tiedot on listattu seuraavassa +kappaleessa. + +## Kumppanilta tarvittavat tekniset tiedot + +
+
(1) Kumppanin palvelun nimi
+
+ Suomeksi, ruotsiksi ja englanniksi. Tällä nimellä palvelu esitetään OPH:n käyttöliittymissä kansalaiselle suostumusta häneltä kysyttäessä. +
+
(2) Kansalaiselta tarvittavat tiedot, Lista OAuth2 scopeja
+
+ Tieto siitä, mitä henkilö- ja opiskeluoikeustietoja kumppanille pitäisi sallia. Ks. myöhempi Scopet-kappale. +
+
(3) Paluuosoitteet, OAuth2 redirect_urit
+
+ Nämä voivat olla eri testiympäristössä ja tuotannossa. Osoitteita voi olla monia, ja testiympäristössä voidaan sallia myös localhost-osoitteita kehityksen helpottamiseksi. +
+
(4) Mahdolliset ketjutetut paluuosoitteet
+
+ Jos redirect_uriin KOSKI-palvelusta tuleva pyyntö aiheuttaa uusia uudelleenohjauksia kumppanin palvelun sisällä, täytyy nekin osoitteet ottaa huomioon KOSKI-palvelun CSP:ssä (Content Security Policy) +
+
(5) mTLS-client-sertifikaatin tunnistetieto
+
+ Subject distinguished name, subject dn. Sertifikaatin tulee olla yleisesti tunnetun CA:n tai DVV:n CA:n allekirjoittama. Tämä voi olla eri testiympäristössä ja tuotannossa. KOSKI-palvelu tarvitsee vain subject-nimen, mutta kätevintä voi olla toimittaa + fullchain.pem-tiedosto. Sertifikaatin uusimisprosessi kannattaa rakentaa niin, että subject-nimi ei siinä muutu, jolloin KOSKI-palvelulle ei tarvitse toimittaa uutta nimeä sertifikaatin uusimisen jälkeen. +
+
(6) Kumppanin palvelun ip-osoitteet
+
+ IP-osoitteet, joista liikenne sallitaan. Nämä voivat olla eri testiympäristössä ja tuotannossa. +
+
+ +## Tiedot rajapinnasta + +
+
OAuth2-palvelun osoitteet ja muut metatiedot
+
+ Ks. sisältö linkistä {{var:baseUrl}}/omadata-oauth2/.well-known/oauth-authorization-server +
+ +
Resource endpoint
+
+ {{var:luovutuspalveluBaseUrl}}/koski/api/omadata-oauth2/resource-server +
Resource endpoint:ia käytetään samalla mTLS-tunnistuksella kuin OAuth2-rajapinnan token endpoint:ia. +
+ +
client_id
+
+ Saat KOSKI-tiimiltä viimeistään OPH:n hyväksynnän ja edellisen kappaleen teknisten tietojen toimittamisen jälkeen. +
+
+ +## Scopet ja palautettava data + +Listan kaikista tuetuista scopeista saa edellisessä kappaleessa mainitusta metatieto-linkistä. + +Toistaiseksi **OPISKELUOIKEUDET_**-scopeja ei voi käyttää kuin yhtä kerrallaan. + +
+
OPISKELUOIKEUDET_SUORITETUT_TUTKINNOT
+
+ opiskeluoikeudet-taulukossa palautetaan oppijan suoritetut tutkinnot. Skeema: {{var:baseUrl}}/json-schema-viewer/?schema=suoritetut-tutkinnot-oppija-schema.json + Lisätietoja, ks. https://wiki.eduuni.fi/pages/viewpage.action?pageId=371305841. +
+
OPISKELUOIKEUDET_AKTIIVISET_JA_PAATTYNEET_OPINNOT
+
+ opiskeluoikeudet-taulukossa palautetaan oppijan aktiiviset ja päättyneet opinnot. Skeema: {{var:baseUrl}}/json-schema-viewer/?schema=aktiiviset-ja-paattyneet-opinnot-oppija-schema.json + Lisätietoja, ks. https://wiki.eduuni.fi/pages/viewpage.action?pageId=371305841. +
+
OPISKELUOIKEUDET_KAIKKI_TIEDOT
+
+ opiskeluoikeudet-taulukossa palautetaan kaikki oppijan opiskeluoikeustiedot, skeema: {{var:baseUrl}}/json-schema-viewer#koski-oppija-schema.json +
+
HENKILOTIEDOT_HETU
+
+ Palautettavassa json-objektissa on henkilö.hetu +
+
HENKILOTIEDOT_NIMI
+
+ Palautettavassa json-objektissa on henkilö.etunimet, henkilö.sukunimi ja henkilö.kutsumanimi +
+
HENKILOTIEDOT_SYNTYMAAIKA
+
+ Palautettavassa json-objektissa on henkilö.syntymäaika, muodossa YYYY-MM-DD +
+
HENKILOTIEDOT_KAIKKI_TIEDOT
+
+ Palautettavassa json-objektissa on kaikki em. henkilötiedot. +
+
+ +Esimerkki palautettavan datan rakenteesta: + + { + "henkilö": { + "hetu": "210281-9988", + "syntymäaika": "1981-02-21", + "etunimet": "Testi Henkilö", + "sukunimi": "Testinen", + "kutsumanimi": "Testi" + }, + "opiskeluoikeudet": [ + { + "oid": "1.2.246.562.15.20709430670", + "oppilaitos": { + "oid": "1.2.246.562.10.51720121923", + ... + }, + ... + "suoritukset": [ + { + "koulutusmoduuli": { + "tunniste": { + "koodiarvo": "374111", + ... + }, + ... + }, + "suoritustapa": { + ... + }, + "vahvistus": { + "päivä": "2019-06-01" + }, + "tyyppi": { + "koodiarvo": "ammatillinentutkinto", + ... + }, + ... + } + ], + "tyyppi": { + "koodiarvo": "ammatillinenkoulutus", + ... + } + }, + ... + ] + } + + +## Huomioita datan käsittelystä + +Kumppanin vastuulla on tarkistaa palautettujen henkilötietojen perusteella, että se sai odottamansa henkilön tiedot. Mitään teknistä estettä ei ole sille, etteikö kansalainen voisi pyytää +esim. ystäväänsä tekemään vahvaa tunnistautumista puolestaan, jolloin KOSKI-järjestelmä palauttaa kumppanille kyseisen ystävän tiedot. + +## Esimerkkiapplikaatio + +Osoitteessa [https://github.com/Opetushallitus/koski/tree/master/omadata-oauth2-sample](https://github.com/Opetushallitus/koski/tree/master/omadata-oauth2-sample) on rajapintaa käyttävän esimerkkiapplikaation lähdekoodi. + +OPH:n testiympäristöissä esimerkkiapplikaatio pyörii osoitteissa: + +* DEV-ympäristö: [https://oph-koski-omadataoauth2sample-dev.testiopintopolku.fi/](https://oph-koski-omadataoauth2sample-dev.testiopintopolku.fi/) +* QA-ympäristö: [https://oph-koski-omadataoauth2sample-qa.testiopintopolku.fi/](https://oph-koski-omadataoauth2sample-qa.testiopintopolku.fi/) + +## Linkkejä spesifikaatioihin + +[RFC6749 The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749) + +[RFC8705 OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://www.rfc-editor.org/rfc/rfc8705) + +[RFC7636 Proof Key for Code Exchange by OAuth Public Clients](https://www.rfc-editor.org/rfc/rfc7636) + +[RFC8414 OAuth 2.0 Authorization Server Metadata](https://www.rfc-editor.org/rfc/rfc8414) + +[OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) diff --git a/src/main/scala/fi/oph/koski/documentation/DocumentationApiServlet.scala b/src/main/scala/fi/oph/koski/documentation/DocumentationApiServlet.scala index afffde2f31..3e08bbeb28 100644 --- a/src/main/scala/fi/oph/koski/documentation/DocumentationApiServlet.scala +++ b/src/main/scala/fi/oph/koski/documentation/DocumentationApiServlet.scala @@ -12,6 +12,7 @@ import fi.oph.koski.massaluovutus.suoritusrekisteri.SureResponse import fi.oph.koski.massaluovutus.valintalaskenta.ValintalaskentaResult import fi.oph.koski.migri.MigriSchema import fi.oph.koski.massaluovutus.{QueryDocumentation, QueryResponse} +import fi.oph.koski.omadataoauth2.OmaDataOAuth2Documentation import fi.oph.koski.schema.KoskiSchema import fi.oph.koski.servlet.{KoskiSpecificApiServlet, NoCache} import fi.oph.koski.suoritusjako.aktiivisetjapaattyneetopinnot.AktiivisetJaPäättyneetOpinnotSchema @@ -39,7 +40,7 @@ class DocumentationApiServlet(application: KoskiApplication) extends KoskiSpecif } get("/sections.html") { - KoskiTiedonSiirtoHtml.htmlTextSections ++ QueryDocumentation.htmlTextSections(application) + KoskiTiedonSiirtoHtml.htmlTextSections ++ QueryDocumentation.htmlTextSections(application) ++ OmaDataOAuth2Documentation.htmlTextSections(application) } get("/apiOperations.json") { diff --git a/src/main/scala/fi/oph/koski/documentation/DocumentationServlet.scala b/src/main/scala/fi/oph/koski/documentation/DocumentationServlet.scala index c4e7785baf..32a2d397f1 100644 --- a/src/main/scala/fi/oph/koski/documentation/DocumentationServlet.scala +++ b/src/main/scala/fi/oph/koski/documentation/DocumentationServlet.scala @@ -25,7 +25,7 @@ class DocumentationServlet(implicit val application: KoskiApplication) val frontendValvontaMode: FrontendValvontaMode.FrontendValvontaMode = FrontendValvontaMode(application.config.getString("frontend-valvonta.mode")) - get("^/(|tietomalli|koodistot|rajapinnat/opintohallintojarjestelmat|rajapinnat/luovutuspalvelu|rajapinnat/palveluvayla-omadata|rajapinnat/massaluovutus/koulutuksenjarjestajat|rajapinnat/massaluovutus/oph)$".r)(nonce => { + get("^/(|tietomalli|koodistot|rajapinnat/opintohallintojarjestelmat|rajapinnat/luovutuspalvelu|rajapinnat/palveluvayla-omadata|rajapinnat/massaluovutus/koulutuksenjarjestajat|rajapinnat/massaluovutus/oph|rajapinnat/oauth2/omadata)$".r)(nonce => { htmlIndex("koski-main.js", raamit = virkailijaRaamit, allowIndexing = true, nonce = nonce) }) diff --git a/src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2DiscoveryServlet.scala b/src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2DiscoveryServlet.scala index 5da174b42e..0217527f56 100644 --- a/src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2DiscoveryServlet.scala +++ b/src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2DiscoveryServlet.scala @@ -35,7 +35,7 @@ class OmaDataOAuth2DiscoveryServlet(implicit val application: KoskiApplication) authorization_endpoint = opintopolkuBaseUrl + "/koski/omadata-oauth2/authorize", scopes_supported = findKoodisto("omadataoauth2scope").map(_.koodiArvo.toUpperCase()).sorted, token_endpoint = luovutuspalveluBaseUrl + "/koski/api/omadata-oauth2/authorization-server", - service_documentation = opintopolkuBaseUrl + "/koski/dokumentaatio/rajapinnat/omadata-oauth2" + service_documentation = opintopolkuBaseUrl + "/koski/dokumentaatio/rajapinnat/oauth2/omadata" ) render(metadata) diff --git a/src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2Documentation.scala b/src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2Documentation.scala new file mode 100644 index 0000000000..1af94e025b --- /dev/null +++ b/src/main/scala/fi/oph/koski/omadataoauth2/OmaDataOAuth2Documentation.scala @@ -0,0 +1,55 @@ +package fi.oph.koski.omadataoauth2 + +import fi.oph.koski.config.KoskiApplication +import fi.oph.koski.documentation.Markdown +import fi.oph.koski.log.Logging +import fi.oph.koski.util.TryWithLogging + +import java.net.URL +import scala.io.Source + +// TODO: TOR-2210: Tähän lisää dokumentaatiota, esimerkkejä yms., jonka generointiin voi ottaa lisää mallia massaluovutuksen QueryDocumentation-luokasta +object OmaDataOAuth2Documentation extends Logging { + // Skeema-jsonit + + // HTML-stringit, jotka palautetaan polusta /koski/api/documentation/sections.html + private val sectionSources = Map( + "oauth2_omadata" -> "documentation/omadata_oauth2.md" + ) + + def htmlTextSections(application: KoskiApplication): Map[String, String] = + sectionSources.mapValues(htmlTextSection(application)) + + def htmlTextSection(application: KoskiApplication)(path: String): String = + TryWithLogging.andResources(logger, { use => + val source = use(Source.fromResource(path)).mkString + val html = Markdown.markdownToXhtmlString(source) + addVariableTexts(application, html) + }).getOrElse(missingSection(path)) + + def missingSection(name: String): String = +

Virhe: resurssia {name} ei löydy

+ .toString() + + def addVariableTexts(application: KoskiApplication, markdown: String): String = { + val rootUrl = new URL(application.config.getString("koski.root.url")) + val baseUrl = rootUrl.toString + + val luovutuspalveluRootUrl = application.config.getString("omadataoauth2.luovutuspalveluBaseUrl") match { + case "mock" => new URL("https://localhost:7022") + case url => new URL(url) + } + val luovutuspalveluBaseUrl = luovutuspalveluRootUrl.toString + + val vars = Map( + "baseUrl" -> baseUrl, + "luovutuspalveluBaseUrl" -> luovutuspalveluBaseUrl + ) + + logger.info(vars.toString) + + "\\{\\{var:(.+?)\\}\\}" + .r("name") + .replaceAllIn(markdown, { m => vars.getOrElse(m.group("name"), "!!! NOT FOUND !!!") }) + } +} diff --git a/web/app/dokumentaatio/Dokumentaatio.jsx b/web/app/dokumentaatio/Dokumentaatio.jsx index 3580ee87b8..eba5501001 100644 --- a/web/app/dokumentaatio/Dokumentaatio.jsx +++ b/web/app/dokumentaatio/Dokumentaatio.jsx @@ -114,6 +114,15 @@ const dokumentaatioContentP = (location, contentP) => '' )} +{/* TODO: TOR-2210: Poista kommenttimerkit lisätäksesi navigointiin, kun dokumentaatio on julkaisuvalmis + {naviLink( + '/koski/dokumentaatio/rajapinnat/oauth2/omadata', + 'OAuth2-standardin mukaiset Omadata-rajapinnat', + location, + '' + )} +*/} + {naviLink( '/koski/dokumentaatio/rajapinnat/massaluovutus/koulutuksenjarjestajat', 'Massaluovutusrajapinnat koulutuksenjärjestäjille', @@ -323,3 +332,31 @@ export const dokumentaatioKyselytP = (path) => { ) } } + +export const dokumentaatioOmadataOAuth2P = (path) => { + const basePath = '/koski/dokumentaatio/rajapinnat/oauth2' + const sections = { + omadata: 'oauth2_omadata' + } + const match = Object.keys(sections).find((key) => + path.includes(`${basePath}/${key}`) + ) + if (match) { + return dokumentaatioContentP( + `${basePath}/${match}`, + htmlSectionsP().map((htmlSections) => ({ + content: ( +
+
+
+ ), + title: 'Dokumentaatio - Rajapinnat' + })) + ) + } +} diff --git a/web/app/virkailija/virkailijaRouter.jsx b/web/app/virkailija/virkailijaRouter.jsx index a8e6a185c4..e99ae0ff95 100644 --- a/web/app/virkailija/virkailijaRouter.jsx +++ b/web/app/virkailija/virkailijaRouter.jsx @@ -13,7 +13,7 @@ import { dokumentaatioOpintohallintojärjestelmätP, dokumentaatioLuovutuspalveluP, dokumentaatioPalveluväyläOmadataP, - dokumentaatioKyselytP + dokumentaatioKyselytP, dokumentaatioOmadataOAuth2P } from '../dokumentaatio/Dokumentaatio' import { onlyIfHasReadAccess } from './accessCheck' import { raportitContentP } from '../raportit/Raportit' @@ -66,6 +66,8 @@ export const routeP = locationP return kelaVirkailijaP(path) } else if (path.includes('/koski/dokumentaatio/rajapinnat/massaluovutus')) { return dokumentaatioKyselytP(path) + } else if (path.includes('/koski/dokumentaatio/rajapinnat/oauth2')) { + return dokumentaatioOmadataOAuth2P(path) } }) .toProperty() From 70eac2815a6bf30cd4e5c49968c6ec0f40afdf00 Mon Sep 17 00:00:00 2001 From: Aleksi Ahtiainen Date: Fri, 1 Nov 2024 08:59:14 +0200 Subject: [PATCH 6/6] Korvaa kovakoodattu state ja verifier muuttuvilla; MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Näin client ei aina käytä samaa verifieria ja state:a. Muistivarainen map ei tietenkään ole tuotantokelpoinen ratkaisu, mutta toimii toistaiseksi esimerkkiapplikaatiossa. --- .../test/e2e/openid-client-test.spec.ts | 4 +- .../server/package-lock.json | 37 ++++++++++++++++--- omadata-oauth2-sample/server/package.json | 3 +- .../server/src/apiroutes/openid-api-test.ts | 25 +++++++++++-- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/omadata-oauth2-sample/client/test/e2e/openid-client-test.spec.ts b/omadata-oauth2-sample/client/test/e2e/openid-client-test.spec.ts index 07c3c73e68..485a7f3109 100644 --- a/omadata-oauth2-sample/client/test/e2e/openid-client-test.spec.ts +++ b/omadata-oauth2-sample/client/test/e2e/openid-client-test.spec.ts @@ -59,9 +59,7 @@ test("Standard error from server is displayed when user declines", async ({ // Check that error data is displayed await page.waitForURL("**/api/openid-api-test/form-post-response-cb**") - await expect(page.locator("html")).toContainText( - '{"state":"state-placeholder","error":"access_denied"}', - ) + await expect(page.locator("html")).toContainText('"error":"access_denied"') }) test("Error page displayed in browser when using invalid redirect_uri", async ({ diff --git a/omadata-oauth2-sample/server/package-lock.json b/omadata-oauth2-sample/server/package-lock.json index 8339a92dec..a19cb224bb 100644 --- a/omadata-oauth2-sample/server/package-lock.json +++ b/omadata-oauth2-sample/server/package-lock.json @@ -14,7 +14,8 @@ "express-rate-limit": "^7.4.0", "helmet": "^7.1.0", "openid-client": "^6.1.1", - "undici": "^6.20.1" + "undici": "^6.20.1", + "uuid": "^11.0.2" }, "devDependencies": { "@aws-sdk/types": "^3.654.0", @@ -209,6 +210,19 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@aws-sdk/client-sso": { "version": "3.658.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.658.0.tgz", @@ -1477,6 +1491,19 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@smithy/middleware-serde": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.6.tgz", @@ -4398,16 +4425,16 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", + "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vary": { diff --git a/omadata-oauth2-sample/server/package.json b/omadata-oauth2-sample/server/package.json index cd23a23932..6f94ae72c9 100644 --- a/omadata-oauth2-sample/server/package.json +++ b/omadata-oauth2-sample/server/package.json @@ -30,7 +30,8 @@ "express-rate-limit": "^7.4.0", "helmet": "^7.1.0", "openid-client": "^6.1.1", - "undici": "^6.20.1" + "undici": "^6.20.1", + "uuid": "^11.0.2" }, "devDependencies": { "@aws-sdk/types": "^3.654.0", diff --git a/omadata-oauth2-sample/server/src/apiroutes/openid-api-test.ts b/omadata-oauth2-sample/server/src/apiroutes/openid-api-test.ts index 16844e4c33..96e20733a3 100644 --- a/omadata-oauth2-sample/server/src/apiroutes/openid-api-test.ts +++ b/omadata-oauth2-sample/server/src/apiroutes/openid-api-test.ts @@ -1,5 +1,6 @@ import express, { NextFunction, Request, Response, Router } from 'express' import * as client from 'openid-client' +import { v4 as uuidv4 } from 'uuid' import { buildAuthorizationUrl, fetchAccessToken, @@ -10,18 +11,23 @@ import { ClientError, ResponseBodyError } from 'openid-client' +import { URLSearchParams } from 'url' const router: Router = express.Router() -const code_verifier: string = client.randomPKCECodeVerifier() -const state = 'state-placeholder' +// TODO: TOR-2210: Toistaiseksi vain muistinvarainen map +let verifiers: Map = new Map() const scope: string = 'HENKILOTIEDOT_NIMI HENKILOTIEDOT_SYNTYMAAIKA HENKILOTIEDOT_HETU OPISKELUOIKEUDET_SUORITETUT_TUTKINNOT' router.get('/', async (req: Request, res: Response, next: NextFunction) => { try { - let redirectTo = await buildAuthorizationUrl(code_verifier, state, scope) + const state = uuidv4() + const code_verifier = client.randomPKCECodeVerifier() + verifiers.set(state, code_verifier) + + const redirectTo = await buildAuthorizationUrl(code_verifier, state, scope) res.redirect(redirectTo.href) } catch (err) { @@ -33,7 +39,10 @@ router.get( '/invalid-redirect-uri', async (req: Request, res: Response, next: NextFunction) => { try { - let redirectTo = await buildAuthorizationUrl( + const state = uuidv4() + const code_verifier = client.randomPKCECodeVerifier() + + const redirectTo = await buildAuthorizationUrl( code_verifier, state, scope, @@ -56,6 +65,14 @@ router.post( '/form-post-response-cb', async (req: Request, res: Response, next: NextFunction) => { try { + const params = new URLSearchParams(req.body) + const state = params.get('state') || '' + const code_verifier = (state && verifiers.get(state)) || '' + + if (state) { + verifiers.delete(state) + } + const token = await fetchAccessToken(req, code_verifier, state) const protectedResource = await fetchData(token)