Tässä dokumentissä on selitetty miten scala-sovellukseen saatiin toteutettua CAS-oppijaa käyttävä autentikaatio.

Täältä löytyy kirjastoja joita voi hyödyntää:

Toteutuksen koodi löytyy täältä: https://github.com/Opetushallitus/omatsivut/pull/172

Palveluun toteutettavat muutokset


Itse integraatio on varsin suoraviivaista, sillä scala-utils -kirjastokokoelmasta löytyvä scala-cas (2.11, 2.12) on adaptoitu osana kehitystä toimimaan läpinäkyvästi virkailijapuolen CAS:n lisäksi myös CAS-Oppijan kanssa. Käytännössä ainoana erona kun CasClientistä luodaan uusi instanssi, annetaan sille konfiguraationa CAS-oppijan osoite CAS:n sijaan, jonka jälkeen client toimii halutulla tavalla CAS-Oppijan kanssa.


Esimerkki CasClientin instansoinnista
import fi.vm.sade.utils.cas.{CasClient}
import org.http4s.client.blaze

var casOppijaClient = new CasClient(casOppijaUrl,
                                    blaze.defaultClient,
                                    callerId)

Sessionhallinta


Palvelussa on oltava käyttäjille sessionhallinta, jolla on mahdollista luoda, poistaa ja hakea session tiedot tarvittaessa. Ei mitään erityisen monimutkaista siis. Mikäli sessionhallintaa ei vielä ole, on se suotavaa toteuttaa niin, että vain välttämättömimmät tiedot siirretään itse clientille (yleisimmin käyttäjän selain) asti, eikä esimerkiksi CAS:n käyttäjätunnistetta käytetä, vaan palvelulle luodaan oma sisäinen käyttäjätunniste, joka sidotaan tiettyyn sessiotunnisteeseen, johon puolestaan linkitetään CAS:sta saadut tiedot.

Login

Kirjautumislogiikka CAS-Oppijaa vasten on seuraavanlainen:

  1. Kirjautuminen aloitetaan ohjaamalla käyttäjän selain (HTTP 302) osoitteeseen https://${host.oppija}/cas-oppija/login?service=$1 jostain palvelun tunnetusta osoitteesta, esimerkiksi /login
    • ${host.oppija} on ympäristön juuridomain, esimerkiksi untuvaopintopolku.fi
    • $1 on palvelun kirjautumisen paluuosoite kokonaisuudessaan URL-turvalliseksi prosenttienkoodattuna, johon CAS-Oppija uudelleenohjaa onnistuneesti kirjautuneen käyttäjän
  2. Käyttäjän selaimen ohjautuessa takaisin palvelun /login -polkuun otetaan polusta talteen CAS-Oppijan generoima tiketti (request parameter "ticket")
  3. CAS-Oppijan tiketti validoidaan palvelinpuolella CAS-Oppijaa vasten (endpoint /cas-oppija/serviceValidate), jolloin palvelu saa tiketin ollessa validi vastauksena käyttäjän varsinaiset tiedot


CasClientiä hyödyntäen validointi voi näyttää vaikkapa tältä:


Tiketin validointi
val attrs: Either[Throwable, OppijaAttributes] = casOppijaClient.validateServiceTicket(kirjautumispolku)(ticket, casOppijaClient.decodeOppijaAttributes).handleWith {
  case NonFatal(t) => Task.fail(new AuthenticationFailedException(s"Failed to validate service ticket $ticket", t))
}.attemptRunFor(10000).toEither
attrs match {
  case Right(attrs) => {
    initializeSessionAndRedirect(attrs)
  }
  case Left(t) => {
    BadRequest(t.getMessage)
  }
}


Pseudokoodina login on tämän tyylinen:

Tiketin validointi pseudokoodina
-> Käyttäjä menee selaimella https://opintopolku.fi/JokuSovellus
-> ei löydy sovelluksen sisäistä sessiota
-> käyttäjän selain ohjataan https://opintopolku.fi/cas-oppija/login?service=https://opintopolku.fi/JokuSovellus/SovelluksenLogin
-> cas ohjaa suomi.fi tunnistautumiseen jonka jälkeen kutsutaan edellisessä kohdassa ollutta service query parametrin urlia siten että cas lisää urliin query parametriksi "ticket"
-> selain ohjataan https://opintopolku.fi/JokuSovellus/SovelluksenLogin?ticket=ST-6777-aBcDeFgHiJkLmN123456-cas.1234567890ac

Sovelluksen pään käsittely:

endpoint = /JokuSovellus/SovelluksenLogin
query parametri = ticket : string

var casXmlValidationResult = cas.validateTicket(ticket) // Lähettää tiketin validoitavaksi osoitteeseen /cas-oppija/ServiceValidate
var validation = parseValidationXml(casXmlValidationResult) // Esimerkit onnistuneen ja epäonnistuneen validoinnin vastauksista alapuolella
if(validation.wasSuccessful)
    var userInfo = parseUserInfo(validation) // Jos suomi.fi käyttäjätiedot riittävät
    // var userInfo = someService.findUserByOid(validation.personOid)     // Vaihtoehtoisesti ulkoisesta palvelusta tietojen haku oid:lla
    var sesssionKey = sessionManager.storeSession(userInfo, ticket) // Sovelluksen sisäisen session luonti
    RespondOk("https://opintopolku.fi/JokuSovellus/etusivu", sessionKey)
else
    RespondUnauthorized("tiketin validointi failasi")

kirjautumispolku pitää täsmätä kokonaisuudessaan osoitetta, josta käyttäjä alunperin uudelleenohjattiin kirjautumiseen. Tämä polku sisältää siis koko osoitteen, mukaan lukien protokollan.

Onnistuneen kirjautumisen jälkeen käyttäjä on hyvä uudelleenohjata (HTTP 302) CAS-Oppijan paluuosoitteesta jollekin oikealle sivulle, kuten vaikkapa Opintopolun etusivulle.

Esimerkki cas:n palauttamasta vastauksesta kun tiketin validointi onnistui
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
    <cas:authenticationSuccess>
        <cas:user>suomi.fi#070770-905D</cas:user>
        <cas:attributes>
            <cas:isFromNewLogin>true</cas:isFromNewLogin>
            <cas:mail>antero.asiakas@suomi.fi</cas:mail>
            <cas:authenticationDate>2020-08-18T11:35:38.453760Z[UTC]</cas:authenticationDate>
            <cas:clientName>suomi.fi</cas:clientName>
            <cas:displayName>Antero Asiakas</cas:displayName>
            <cas:givenName>Antero</cas:givenName>
            <cas:VakinainenKotimainenLahiosoiteS>Sepänkatu 11 A 5</cas:VakinainenKotimainenLahiosoiteS>
            <cas:VakinainenKotimainenLahiosoitePostitoimipaikkaS>KUOPIO</cas:VakinainenKotimainenLahiosoitePostitoimipaikkaS>
            <cas:cn>Asiakas Antero OP</cas:cn>
            <cas:notBefore>2020-08-18T11:35:35.788Z</cas:notBefore>
            <cas:personOid>1.2.246.562.24.66085201211</cas:personOid>
            <cas:personName>Asiakas Antero OP</cas:personName>
            <cas:firstName>Antero OP</cas:firstName>
            <cas:VakinainenKotimainenLahiosoitePostinumero>70100</cas:VakinainenKotimainenLahiosoitePostinumero>
            <cas:KotikuntaKuntanumero>297</cas:KotikuntaKuntanumero>
            <cas:KotikuntaKuntaS>Kuopio</cas:KotikuntaKuntaS>
            <cas:notOnOrAfter>2020-08-18T11:40:35.788Z</cas:notOnOrAfter>
            <cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>
            <cas:sn>Asiakas</cas:sn>
            <cas:nationalIdentificationNumber>070770-905D</cas:nationalIdentificationNumber>
        </cas:attributes>
    </cas:authenticationSuccess>
</cas:serviceResponse>
Esimerkki cas:n palauttamasta vastauksesta kun tiketin validointi epäonnistui
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
    <cas:authenticationFailure code="INVALID_TICKET">
        Ticket ST-6777-aBcDeFgHiJkLmN123456-cas.1234567890ac not recognized
    </cas:authenticationFailure>
</cas:serviceResponse>


Logout

Uloskirjautumislogiikka vastaa pitkälti kirjautumisen logiikkaa:

  1. Käyttäjän selain uudelleenohjataan (HTTP 302) osoitteeseen https://${host.oppija}/cas-oppija/logout?service=$1 jostain palvelun tunnetusta osoitteesta, esimerkiksi /logout
    • ${host.oppija} on ympäristön juuridomain, esimerkiksi untuvaopintopolku.fi
    • $1 on palvelun uloskirjautumisen paluuosoite kokonaisuudessaan URL-turvalliseksi prosenttienkoodattuna, johon CAS-Oppija uudelleenohjaa onnistuneesti uloskirjautuneen käyttäjän
  2. Tämän yhteydessä poistetaan tai muuten invalidoidaan käyttäjän palvelukohtainen sessio sekä suoritetaan muut datan siivoamistarpeet


Session poisto kannattaa toteuttaa ennen CAS-Oppijan uloskirjautumiseen ohjaamista sen varmistamiseksi, ettei sessioita jää turhaan roikkumaan vaikka käyttäjä olisikin uloskirjautunut CAS-Oppijan puolella.

Logout-rajapinta on pakollinen muuten Opintopolku ei täytä DVV:n single-sign-out-vaatimuksia.

Pilviympäristöön mahdollisesti toteutettavat muutokset

Shibboleth

Mikäli palvelu on aiemmin käyttänyt käyttäjien tunnistamiseen Shibbolethia, on Shibbolethiin liittyvät proxy-konfiguraatiot poistettava ympäristöstä. Tämä onnistuu cloud-basen CDK:lla;

  1. Poista nginx:stä proxykonfiguraatio (ks. esimerkiksi https://github.com/Opetushallitus/nginx/pull/9)
  2. Deployaa päivitetty nginx CDK:lla haluttuun ympäristöön

Käytännön esimerkki integraatiomuutoksesta omatsivut-palveluun

omatsivut -palveluun toteutettiin siirtymä Shibbolethista CAS-oppijaan tämän dokumentaation pohjatyönä. Kaikki koodimuutokset tähän liittyen on tarkasteltavissa täällä: https://github.com/Opetushallitus/omatsivut/pull/168/files

Ylimalkaisesti ilmaistuna logiikka muuttui seuraavasti:

  • Palvelun sisäinen sessio populoidaan nyt validoidun CAS-tiketin pohjalta eikä suoraan Shibbolethin paluuarvoista
  • Shibboleth-viittaukset poistettiin täysin
  • Aikaisemmin Shibbolethia varten tehdyt palaset (esim. login/logout-polut, paluuosotteiden päättely) uudelleenkäytettiin järkeviltä osin CAS-Oppijan kanssa
  • Shibbolethiin liittyvät uudelleenohjaukset, paluuviestin parsimiset yms. heivattiin bittiroskikseen tarpeettomina ja CasClient adaptoitiin tukemaan callback-pohjaista paluuviestin parsintaa


  • No labels