# Identiteettivarmistus

URL: https://aihio.ai/docs/chatbot/identiteettivarmistus
Kuvaus: Sido kirjautunut käyttäjä kryptografisesti chat-widgettiin. HMAC-käyttäjätiiviste tai JWT – palvelin allekirjoittaa, Aihio vahvistaa.

Ilman identiteettivarmistusta kuka tahansa kävijä voi kutsua `AihioWidget('identify', { user_id: '…' })` selaimen kehittäjätyökalusta millä käyttäjätunnuksella tahansa. Varmistus sitoo istunnon palvelimesi allekirjoittamaan salaisuuteen: Aihio tietää, että kutsuja on oikeasti kirjautunut käyttäjä.

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

## Mistä salaisuus löytyy

Avaa chatbotin **Julkaise**-näkymä ohjauspaneelissa. Sivulla on **Identiteetin varmennus** -osio chat-widgetin upotuskoodin vieressä. Paina **Luo salaisuus** 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.

Suositeltu tokenin elinikä on 1 tunti (`exp = iat + 3600`). Älä myönnä yli 24 tunnin tokeneita.

### 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           |
| `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                 | Esitäyttää esikeskustelulomakkeen      |
| `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,
  }
  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,
    }
    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,
    ];
    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(),
    })
    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))
        .signWith(key)
        .compact();
}
```

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

Kirjoittamalla `stripe_accounts`-taulukon JWT-tokeniin voit liittää käyttäjään Stripe-asiakastiedot. Varmennettu identiteetti toimii tulevien identiteettisidonnaisten toimintojen pohjana, jotka hakevat tilaukset ja laskut ilman erillistä tunnistautumista.

```javascript
jwt.sign(
  {
    user_id: user.id,
    email: user.email,
    stripe_accounts: [{ label: 'Pääasiakkuus', stripe_id: 'cus_xxx' }],
    exp: Math.floor(Date.now() / 1000) + 3600,
  },
  secret,
  { algorithm: 'HS256' },
);
```

> **Tämä on tuleva ominaisuus:**
>
> Identiteettivarmistus ja `identity_verified`-lippu toimivat tänään. Toiminnot,
> jotka lukevat `stripe_accounts`-tietoja (tilaukset, laskut) ovat erillinen
> tuleva ominaisuus. Allekirjoita kenttä jo nyt, niin käyttöönotto on sujuvaa
> kun ominaisuus julkaistaan.

## Identiteetin välittäminen widgetille

Laske arvo palvelimella ja injektoi se sivulle. Kutsu sitten `identify`:

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

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

`identify`-kutsun voi tehdä milloin tahansa `init`-kutsun jälkeen. Jos kutsut sitä ennen kuin bundle on latautunut, komento asetetaan jonoon ja suoritetaan automaattisesti käynnistyksen yhteydessä.

Tyhjennä identiteetti uloskirjautumisen yhteydessä:

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

## 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. Tämä vastaa Stripen webhook-salaisuuden kierrätysmallia: voit päivittää allekirjoituskoodin ympäristöihin ennen kuin vanha salaisuus vanhenee.

1. **Kierrätä salaisuus**

   Paina **Kierrätä salaisuus** Julkaise-näkymän Identiteetin varmennus -osiossa.
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). Voit ottaa pakotuksen käyttöön chatbotin **Julkaise → Suojaus** -välilehdeltä.

- **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ä, keskustelu sidotaan varmennettuun `external_id`-tunnisteeseen. Uudelleenkäytetty `reference_id`, jonka tallennettu identiteetti ei vastaa varmennettua tokenia, aloittaa uuden keskustelun sen sijaan että liittyisi vanhaan. Näin yksi kävijä ei voi jatkaa toisen kävijän yksityistä keskustelua.

Pakotus avautuu vasta, kun Aihio on nähnyt vähintään yhden validin tokenin, joten et voi lukita itseäsi ulos ottamalla sen käyttöön ennen kuin integraatiosi on toiminnassa.

## Turvallinen yhteys ja istunnon kesto

- **Vain turvallinen yhteys.** Kun tämä on päällä, chat-widget lähettää identiteettiotsikot ja tallentaa identiteetin vain HTTPS-yhteydellä. Tavallisella HTTP-sivulla se ei lähetä identiteettiä, ja palvelin hylkää varmennuksen.
- **Istunnon kesto.** Aseta tokenin suurin hyväksytty ikä (60 sekunnista 30 päivään). Chat-widget tyhjentää tallennetun identiteetin iän ylittyessä, ja palvelin hylkää rajaa vanhemman tokenin, vaikka sen oma `exp` olisi yhä voimassa.

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

Nykyinen toteutus on **avoin käyttöönottoa varten**: jos allekirjoitusta ei toimiteta tai varmennus epäonnistuu, istunto jatkuu `identity_verified = false` -tilassa eikä sitä hylätä.

Tämä tarkoittaa, että varmistamaton `user_id` tai sähköposti hyväksytään näyttövihjeinä, mutta teknisesti hyökkääjän muutettavissa olevia arvoja. Käsittele identiteettitietoja luotettavina **vain** kun keskustelun `identity_verified`-lippu on `true`.

## 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 chatbotin salaisuus yksinomaan palvelimelle. Älä sisällytä sitä selainpuolen JavaScript-koodiin tai versiohallintaan.
- Aseta `exp` jokaiseen JWT-tokeniin (suositus 1 h).
- Kierrätä salaisuus heti, jos se paljastuu (Julkaise-näkymän Identiteetin varmennus -osion kautta).
- Tarkista `identity_verified = true` toimintokäsittelijöissäsi ennen tilitasoisen datan palauttamista.
- Käytä HMAC-funktiosi oletustulosteen pieniä kirjaimia sisältävää heksadesimaalia.
