# Identiteettivarmistus

URL: https://aihio.ai/ohjekeskus/tekoalyagentti/identiteettivarmistus\
Kuvaus: Välitä kirjautuneen käyttäjän tiedot palvelimen allekirjoittamina ja valitse oikea varmennustapa keskusteluikkunalle.

Ilman identiteettivarmistusta kävijä voi antaa `identify`-kutsussa minkä tahansa käyttäjätunnisteen. Allekirjoita tunnistetiedot omalla palvelimellasi vasta, kun olet tarkistanut kirjautumisen. Aihio tarkistaa allekirjoituksen, ei oman sovelluksesi kirjautumistapaa.

Aihio tukee kahta menetelmää: **HMAC-käyttäjätiivistettä** ja **JWT HS256 -tokenia**.

> **Varoitus — Valitse JWT yksityisiä tietoja käyttäville toiminnoille:**
>
> HMAC-tiivisteessä ei ole vanhenemisaikaa, ja selaimelle annettua tiivistettä
> voi käyttää uudelleen. Se sopii näyttötietojen personointiin, ei yksin
> yksityisten tietojen käyttöoikeudeksi. Käytä lyhytikäistä JWT:tä, varmennettua
> identiteettiä vaativaa toimintoa ja oman rajapintasi käyttöoikeustarkistuksia.

## Mistä salaisuus löytyy

Avaa Oma Aihio -palvelussa agentin **Asetukset → Suojaus**. Paina
**Identiteetin varmennus** -osiossa **Luo avain** ja kopioi arvo heti
talteen: se näytetään vain kerran. Salaisuus näyttää esimerkiksi tältä:
`aihio_idv_…`.

Tallenna salaisuus palvelimesi ympäristömuuttujaksi (esim. `AIHIO_IDENTITY_SECRET`). Älä koskaan sisällytä sitä selainpuolen JavaScript-koodiin tai versiohallintaan.

## Menetelmä A: HMAC-käyttäjätiiviste

Laske `HMAC-SHA256(salaisuus, user_id)` palvelimellasi ja välitä tulos `user_hash`-kentässä. Tulosteen **on oltava pienet kirjaimet sisältävä heksadesimaali**. Aihio hylkää isolla kirjoitetun heksan.

Kaikki alla olevat esimerkit tuottavat oikean muodon oletuksena.

### Node.js

```javascript
const crypto = require('crypto');

function computeUserHash(secret, userId) {
  return crypto.createHmac('sha256', secret).update(userId).digest('hex');
}
```

### Python

```python
import hmac, hashlib

def compute_user_hash(secret: str, user_id: str) -> str:
    return hmac.new(
        secret.encode('utf-8'),
        user_id.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
```

### PHP

```php
function compute_user_hash(string $secret, string $user_id): string {
    return hash_hmac('sha256', $user_id, $secret);
}
```

### Ruby

```ruby
require 'openssl'

def compute_user_hash(secret, user_id)
  OpenSSL::HMAC.hexdigest('SHA256', secret, user_id)
end
```

## Menetelmä B: JWT HS256 -token

Allekirjoita JWT `HS256`-algoritmilla chatbotin salaisuudella. `exp`-väite on **pakollinen**: Aihio hylkää tokenin ilman sitä. Kellopoikkeama on 30 sekuntia.

Valitse lyhyt, käyttötarkoitukseen sopiva voimassaoloaika, esimerkiksi yksi tunti (`exp = iat + 3600`). Tämä on esimerkkivalinta, ei palvelun kaikille tokeneille asettama enimmäisaika. Lisää myös `iat`, jotta asetettu tokenin enimmäisikä voidaan tarkistaa palvelimella.

### Tuetut JWT-väitteet

| Väite               | Tyyppi             | Pakollinen         | Huomio                                                     |
| ------------------- | ------------------ | ------------------ | ---------------------------------------------------------- |
| `user_id` tai `sub` | merkkijono         | kyllä (jompikumpi) | Käyttäjätunnus omassa järjestelmässäsi                     |
| `external_id`       | merkkijono         | ei                 | `user_id`/`sub`-alias                                      |
| `exp`               | numero (Unix-aika) | kyllä              | Vanheneminen; 30 s poikkeama                               |
| `iat`               | numero (Unix-aika) | Suositeltu         | Myöntämisaika tokenin enimmäisiän tarkistusta varten       |
| `nbf`               | numero (Unix-aika) | ei                 | Voimassa aikaisintaan; 30 s poikkeama                      |
| `email`             | merkkijono         | ei                 | Esitäyttää esikeskustelulomakkeen                          |
| `name`              | merkkijono         | ei                 | Esitäyttää esikeskustelulomakkeen                          |
| `phonenumber`       | merkkijono         | ei                 | Varmennin hyväksyy kentän; ei lupaus lomakkeen esitäytöstä |
| `custom_attributes` | objekti            | ei                 | Vapaamuotoiset lisätiedot                                  |
| `stripe_accounts`   | taulukko           | ei                 | Käyttäjän Stripe-tilit (ks. alla)                          |

Vain `HS256` hyväksytään. RS256-, ES256- tai `alg: none` -tokeneja ei hyväksytä.

## Allekirjoita JWT palvelimella

Allekirjoita token chatbotin salaisuudella `HS256`-algoritmilla. Aseta `exp` (suositus 1 tunti). `custom_attributes` on vapaaehtoinen.

### Node.js (`jsonwebtoken`)

Asennus: `npm install jsonwebtoken`

```javascript
const jwt = require('jsonwebtoken');

function signIdentityToken(secret, user) {
  return jwt.sign(
    {
      user_id: user.id,
      email: user.email,
      name: user.name,
      custom_attributes: { plan: user.plan },
    },
    secret,
    { algorithm: 'HS256', expiresIn: '1h' },
  );
}
```

### Ruby on Rails (`jwt`-kirjasto)

Asennus: `bundle add jwt`

```ruby
require 'jwt'

def sign_identity_token(secret, user)
  payload = {
    user_id: user.id,
    email: user.email,
    name: user.name,
    custom_attributes: { plan: user.plan },
    exp: Time.now.to_i + 3600,
    iat: Time.now.to_i,
  }
  JWT.encode(payload, secret, 'HS256')
end
```

### Django / Python (`PyJWT`)

Asennus: `pip install PyJWT`

```python
import time
import jwt

def sign_identity_token(secret: str, user) -> str:
    payload = {
        'user_id': user.id,
        'email': user.email,
        'name': user.name,
        'custom_attributes': {'plan': user.plan},
        'exp': int(time.time()) + 3600,
        'iat': int(time.time()),
    }
    return jwt.encode(payload, secret, algorithm='HS256')
```

### PHP (`firebase/php-jwt`)

Asennus: `composer require firebase/php-jwt`

```php
use Firebase\JWT\JWT;

function sign_identity_token(string $secret, $user): string {
    $payload = [
        'user_id' => $user->id,
        'email' => $user->email,
        'name' => $user->name,
        'custom_attributes' => ['plan' => $user->plan],
        'exp' => time() + 3600,
        'iat' => time(),
    ];
    return JWT::encode($payload, $secret, 'HS256');
}
```

### Go (`golang-jwt`)

Asennus: `go get github.com/golang-jwt/jwt/v5`

```go
import (
    "time"

    "github.com/golang-jwt/jwt/v5"
)

func SignIdentityToken(secret string, user User) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "user_id":           user.ID,
        "email":             user.Email,
        "name":              user.Name,
        "custom_attributes": map[string]any{"plan": user.Plan},
        "exp":               time.Now().Add(time.Hour).Unix(),
        "iat":               time.Now().Unix(),
    })
    return token.SignedString([]byte(secret))
}
```

### Java (`jjwt`)

Asennus (Maven): `io.jsonwebtoken:jjwt-api`, `jjwt-impl`, `jjwt-jackson` (runtime).

```java
import io.jsonwebtoken.Jwts;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

String signIdentityToken(String secret, User user) {
    SecretKeySpec key = new SecretKeySpec(
        secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
    return Jwts.builder()
        .claim("user_id", user.getId())
        .claim("email", user.getEmail())
        .claim("name", user.getName())
        .claim("custom_attributes", Map.of("plan", user.getPlan()))
        .expiration(new Date(System.currentTimeMillis() + 3_600_000))
        .issuedAt(new Date())
        .signWith(key)
        .compact();
}
```

### Stripe-tilien välittäminen (tuleva ominaisuus)

Varmennin hyväksyy `stripe_accounts`-kentän, mutta se ei ota käyttöön Stripe-tilausten tai laskujen hakua. Älä lähetä näitä tietoja varmuuden vuoksi. Käytä vain nykyisen integraatiosi tarvitsemia kenttiä.

## Identiteetin välittäminen widgetille

Laske arvo palvelimella ja välitä se kirjautuneelle käyttäjälle suojatun vastauksen kautta. Korvaa alla olevat paikkamerkit näillä arvoilla. Älä välitä allekirjoitussalaisuutta. Valitse vain toinen esimerkin menetelmistä:

```javascript
// Menetelmä A: HMAC-käyttäjätiiviste
window.AihioWidget('identify', {
  externalId: currentUser.id, // Myös 'user_id' hyväksytään; externalId on ensisijainen.
  email: currentUser.email,
  name: currentUser.name,
  user_hash: '{{ palvelimelta_laskettu_tiiviste }}',
});

// Menetelmä B: JWT-token
window.AihioWidget('identify', {
  token: '{{ palvelimelta_allekirjoitettu_jwt }}',
});
```

`currentUser` tarkoittaa oman sovelluksesi kirjautunutta käyttäjää, ei selaimesta vapaasti valittavaa käyttäjätunnistetta. `identify`-kutsun voi tehdä `init`-kutsun jälkeen. Jos keskusteluikkunan koodi ei ole vielä latautunut, upotuskoodin komentojono suorittaa kutsun käynnistyksen yhteydessä.

Tyhjennä identiteetti uloskirjautumisen yhteydessä. Tämä ei poista palveluun tallennettuja keskusteluja:

```javascript
window.AihioWidget('resetUser');
```

### Kirjautuneen kävijän keskustelun jatkuminen

Keskustelu, jonka varmennettu kävijä aloittaa, sidotaan hänen `external_id`-tunnisteeseensa, oli pakotus päällä tai ei. Vain pyyntö, jonka JWT tai käyttäjätiiviste todistaa saman `external_id`-tunnisteen, voi jatkaa keskustelua, lukea asiakaspalvelijan vastauksia tai jättää keskusteluun viestin. Kävijä ilman todistetta tai toinen kirjautunut käyttäjä ei voi jatkaa keskustelua, vaikka tietäisi keskustelun tunnisteen. Hän voi kuitenkin aloittaa oman keskustelun.

JWT vanhenee, joten anna keskusteluikkunalle tapa hakea saman käyttäjän uusi token:

```javascript
window.AihioWidget('identify', {
  token: '{{ palvelimelta_allekirjoitettu_jwt }}',
  getToken: async () => {
    const response = await fetch('/api/aihio-token');
    return (await response.json()).token;
  },
});
```

Kun palvelin hylkää vanhentuneen tokenin tai tokenin, joka on suurinta hyväksyttyä ikää vanhempi, keskusteluikkuna kutsuu `getToken`-funktiota kerran ja toistaa pyynnön uudella tokenilla. Samaan aikaan tehdyt pyynnöt käyttävät yhteistä kutsua, jonka vastausta ikkuna odottaa enintään 10 sekuntia. Jos `getToken` puuttuu, epäonnistuu tai ei palauta tokenia, keskusteluikkuna pyytää kävijää kirjautumaan uudelleen sisään eikä toista pyyntöä. Keskustelu asiakaspalvelijan kanssa pysyy auki ja jatkuu, kun `identify` antaa kelvollisen tokenin.

## Salaisuuden kierrätys

Salaisuuden kierrätys **ei** välittömästi mitätöi vanhaa salaisuutta. Edellinen salaisuus on voimassa 24 tuntia kierrätyksen jälkeen. Päivitä allekirjoituskoodi kaikkiin ympäristöihin ennen vanhan salaisuuden vanhenemista.

> **Vaara — Jos salaisuus on paljastunut:**
>
> Älä käytä tavallista kierrätystä, koska se pitää vanhan salaisuuden voimassa
> 24 tuntia. Valitse **Asetukset → Suojaus → Identiteetin varmennus → Poista**
> ja sen jälkeen **Luo avain**. Päivitä uusi arvo palvelimelle ennen
> varmistuksen pakottamista uudelleen.

### Tavallinen kierrätys ilman salaisuuden paljastumista

1. **Kierrätä salaisuus**

   Paina **Kierrätä** kohdassa **Asetukset → Suojaus → Identiteetin varmennus**.
2. **Päivitä ympäristömuuttuja**

   Päivitä `AIHIO_IDENTITY_SECRET` palvelimesi ympäristöön uudella arvolla.
3. **Ota muutos käyttöön**

   Ota uusi ympäristömuuttuja käyttöön. Vanha salaisuus jatkaa istuntojen varmennusta rinnakkain 24 tunnin ajan.
4. **Odota 24 tuntia**

   24 tunnin kuluttua vanha salaisuus lakkaa toimimasta. Holvimerkintä säilytetään, mutta sitä ei enää käytetä varmennuksessa.

## Pakota identiteettivarmistus

Oletuksena varmistus on avoin (fail-open). Pakotuksen voi ottaa käyttöön agentin
**Asetukset → Suojaus** -näkymästä.

- **Pakota identiteettivarmistus.** Pyyntö, joka väittää identiteettiä (`user_id`/`external_id`, `email`, `token` tai `user_hash`) ilman validia allekirjoitusta, hylätään HTTP 403:lla. Anonyymit kävijät voivat silti keskustella.
- **Vaadi tunnistautuminen (tiukka).** Jokainen varmistamaton pyyntö hylätään, myös anonyymit.

Kun pakotus on päällä, myös anonyymi keskustelu sidotaan varmennettuun kävijään, joka jatkaa sitä. Sidonta koskee ihmistukea: asiakaspalvelijan vastaukset näkyvät vain samalle varmennetulle kävijälle. Välitä `getToken`-funktio, jotta vanhentunut token uusitaan myös silloin, kun keskustelu on asiakaspalvelijalla.

Ota pakotus käyttöön vasta onnistuneen varmennustestin jälkeen. Testaa erikseen voimassa oleva JWT, vanhentunut JWT ja kävijä ilman tunnistetta. Varmista, että valitsemasi anonyymin käytön raja vastaa tarkoitustasi.

## Turvallinen yhteys ja istunnon kesto

- **Vain turvallinen yhteys.** Kun tämä on päällä, keskusteluikkuna välittää ja tallentaa identiteetin vain HTTPS-yhteydellä. HTTP-sivulla identiteetti jää välittämättä; palvelimen pakotusasetukset ratkaisevat, sallitaanko anonyymi keskustelu.
- **Istunnon kesto.** Aseta tokenin suurin hyväksytty ikä. Palvelin voi tarkistaa iän tokenin numeerisesta `iat`-kentästä. Ilman sitä tämä ikäraja ei korvaa `exp`-vanhenemisaikaa. Sisällytä uusiin tokeneihin sekä `iat` että `exp` ja testaa valitsemasi raja.

## Käyttöönottotapa: toiminto avoimena (fail-open)

Kun pakotus ei ole käytössä, puuttuva tai epäonnistunut varmennus sallii keskustelun jatkumisen `identity_verified = false` -tilassa. Pakotus ja tiukka tunnistautumisvaatimus muuttavat tämän käytöksen edellä kuvatulla tavalla.

Varmistamaton `user_id` tai sähköposti on käyttäjän antama näyttövihje. Pelkkä `identity_verified`-arvo ei kerro, varmennettiinko JWT vai HMAC-tiiviste. Yksityisiä tietoja käsittelevä toiminto edellyttää JWT:tä ja erillistä käyttöoikeuden tarkistusta.

## Allekirjoitetut vs allekirjoittamattomat tiedot

Vain JWT:n sisällä allekirjoitetut tiedot ovat varmennettuja ja luotettavia. Kaikki muu on näyttövihje, jonka lähettäjä voi väärentää.

| Allekirjoitettu (luotettava)                                     | Allekirjoittamaton (vain vihje)                 |
| ---------------------------------------------------------------- | ----------------------------------------------- |
| `user_id` / `external_id`                                        | Prechat-lomakkeen nimi, sähköposti ja suostumus |
| `email`, `name`, `custom_attributes` (kun ne ovat JWT:n sisällä) | Mikä tahansa `x-identify`-otsikko ilman JWT:tä  |

HMAC-käyttäjätiiviste (tapa A) varmentaa vain `external_id`-arvon. Jos haluat luottaa `email`-, `name`- tai mukautettuihin kenttiin, allekirjoita ne JWT:n sisällä (tapa B). Lähettäjän antamat kentät hyväksytään näyttövihjeinä. Niitä ei koskaan tallenneta varmennettuina, ja keskustelun `identity_verified`-lippu pysyy arvossa `false`, ellei validia allekirjoitusta ole.

## Tietoturvatarkistuslista

- Tallenna agentin salaisuus yksinomaan palvelimelle. Älä sisällytä sitä selainpuolen JavaScript-koodiin tai versiohallintaan.
- Aseta `iat` ja lyhyt `exp` jokaiseen JWT-tokeniin.
- Jos salaisuus paljastuu, valitse **Asetukset → Suojaus → Identiteetin varmennus → Poista**, luo uusi avain, ota se käyttöön palvelimella ja varmenna toiminta ennen pakotuksen palauttamista. Älä käytä tavallista kierrätystä salaisuuden mitätöintiin.
- Vaadi yksityisiä tietoja käsittelevältä toiminnolta JWT-varmennus. Tarkista omassa rajapinnassasi lisäksi, että käyttäjällä on oikeus juuri pyydettyyn tietoon.
- Käytä HMAC-funktiosi oletustulosteen pieniä kirjaimia sisältävää heksadesimaalia.
## Seuraavat vaiheet

- [Mukautetut HTTP-toiminnot](/ohjekeskus/tekoalyagentti/toiminnot) — Määrittele HTTPS-rajapintakutsu, jonka agentti täyttää ja palvelin suorittaa keskustelun aikana. Tunnukset säilytetään salattuina ja kutsut on suojattu.
