Skip to content

Commit 587d14a

Browse files
deploy.shclaude
andcommitted
Document the embedded sign-in form
Adds an "Embedded sign-in" section between Authentication and Identification: the widget runs the authentication in an iframe on the integrator's own page instead of redirecting, using the same service, credentials, scopes and ID token — only the start of the flow differs. Covers when NOT to use it (public/PKCE clients cannot: creating a session needs the client secret), enabling it and allowlisting origins in eeID manager, the three API calls with parameters and samples, the widget element with its attributes and events, redeeming the single-use result token, return_url for the methods that must leave the frame, theming, the error table, rate limits and the test environment. Two things integrators cannot guess are called out explicitly: the code never reaches the browser (their backend redeems a single-use token for it), and they must take the iframe down on eeid:success or a completed sign-in looks hung. Links the PHP example at github.com/internetee/eeid_php_embedded_demo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GP9ZnyKyjjFfBmv3evRGV3
1 parent 13ea750 commit 587d14a

1 file changed

Lines changed: 358 additions & 0 deletions

File tree

source/index.html.md

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,364 @@ Start up rails server and point browser to [http://localhost:3000](http://localh
14611461
bin/dev
14621462
```
14631463

1464+
# Embedded sign-in
1465+
1466+
The embedded sign-in form (the *eeID widget*) runs the authentication inside an iframe on your
1467+
own page, instead of redirecting the user to eeID and back. The user never leaves your site.
1468+
1469+
Everything else stays the same: the same service, the same client credentials, the same scopes
1470+
and the same ID token. Only the beginning of the flow differs — instead of sending the browser
1471+
to the [authentication request](#authentication-request) URL, your backend asks eeID for a
1472+
session and your page frames it.
1473+
1474+
**The authorization code never reaches the browser.** Your backend creates the session with your
1475+
client credentials, the widget hands your page a single-use `result_token`, and your backend
1476+
exchanges that token for the code. A user with developer tools open sees nothing they can replay.
1477+
1478+
A complete, runnable integration in PHP is published at
1479+
[github.com/internetee/eeid_php_embedded_demo](https://github.com/internetee/eeid_php_embedded_demo).
1480+
1481+
## Should you use it?
1482+
1483+
Use the embedded form when you want the sign-in to feel part of your own page, and you have a
1484+
backend that can hold a client secret.
1485+
1486+
Keep the classic [redirect flow](#eeid-authentication) when your client is public (a single-page
1487+
app or a mobile application using [PKCE](#pkce-proof-key-for-code-exchange)), or when you cannot
1488+
run the two server-side calls the embedded form requires. The embedded form is **not** available
1489+
to public clients: creating a session requires your client secret.
1490+
1491+
Both flows can be enabled on the same service at the same time.
1492+
1493+
## Requirements
1494+
1495+
- A registered and approved `Authentication` service — see [Creating a new service](#creating-a-new-service)
1496+
- Its `Client ID` and `Secret`, used from your backend only
1497+
- **Embedded mode enabled** on the service, with your page's origin allowlisted (below)
1498+
1499+
## Enabling embedded mode
1500+
1501+
In [eeID manager](https://eeid.ee), open your service and edit it:
1502+
1503+
1. Tick **Enable embedded widget**.
1504+
2. In **Embedded widget allowed origins**, add every origin that will frame the widget, one per
1505+
line — for example `https://www.example.com`.
1506+
3. Save.
1507+
1508+
Only the listed origins may frame the widget or receive its messages. A page served from any
1509+
other origin is refused by the browser, and session creation is rejected.
1510+
1511+
An origin is a scheme, a host and an optional port — nothing else:
1512+
1513+
* `https://www.example.com` — correct
1514+
* `https://www.example.com:8443` — correct
1515+
* `https://www.example.com/signin` — rejected, a path is not part of an origin
1516+
* `https://*.example.com` — rejected, wildcards cannot be used to address a browser window
1517+
1518+
`http://` is accepted only for local development origins such as `http://localhost:8081`.
1519+
1520+
<aside class="notice">
1521+
Origins are compared exactly. <code>https://example.com</code> and <code>https://www.example.com</code>
1522+
are different origins, and so are <code>http://localhost:8081</code> and <code>http://127.0.0.1:8081</code>.
1523+
</aside>
1524+
1525+
## How it works
1526+
1527+
Four steps. Two of them are server-to-server calls from your backend, one is the widget in your
1528+
page, and the last is the ordinary OAuth2 token request you already make today.
1529+
1530+
1. Your backend calls **`POST /api/embedded/sessions`** with your client credentials and receives
1531+
a `session_token`.
1532+
2. Your page loads the widget script and renders `<eeid-widget>` with that token. The user picks
1533+
a method and authenticates inside the iframe.
1534+
3. The widget tells your page it succeeded, handing over a single-use `result_token`.
1535+
4. Your backend calls **`POST /api/embedded/sessions/{result_token}/redeem`** to obtain the
1536+
OAuth2 `code`, then exchanges it at the [identity token request](#identity-token-request) exactly as in
1537+
the redirect flow.
1538+
1539+
The browser never talks to the OAuth2 server: eeID performs the authorization request and the
1540+
login/consent steps server-side. That is what allows the flow to work when third-party cookies
1541+
are blocked.
1542+
1543+
## Creating a session
1544+
1545+
> Create a session
1546+
1547+
```shell
1548+
curl -X POST https://auth.eeid.ee/api/embedded/sessions \
1549+
-u "$EEID_CLIENT_ID:$EEID_CLIENT_SECRET" \
1550+
-H "Content-Type: application/json" \
1551+
-d '{
1552+
"redirect_uri": "https://www.example.com/auth/callback",
1553+
"scope": "openid",
1554+
"state": "f3b2c3e7f4cf0bed3a783ed6ece617e3",
1555+
"nonce": "9a1f4b0c2d8e6f37",
1556+
"locale": "en",
1557+
"return_url": "https://www.example.com/signin"
1558+
}'
1559+
```
1560+
1561+
> Response
1562+
1563+
```json
1564+
{
1565+
"session_token": "gW7s8x1Qm2...",
1566+
"expires_in": 600,
1567+
"allowed_methods": null,
1568+
"locales": ["en", "et", "ru"]
1569+
}
1570+
```
1571+
1572+
URL: `POST https://auth.eeid.ee/api/embedded/sessions`
1573+
1574+
Authentication: HTTP Basic, with your `Client ID` as the user name and your `Secret` as the
1575+
password. **This call must be made from your backend.** Never expose the secret to a browser.
1576+
1577+
Required parameters:
1578+
1579+
- `redirect_uri` - one of the redirection URLs registered on the service. The browser is not sent
1580+
there in the embedded flow, but a real authorization request is started with it and the same
1581+
value is presented again at the token request, so it must match exactly.
1582+
1583+
Optional parameters:
1584+
1585+
- `scope` - authentication scope, defaults to `openid`. Must correspond to the scope selected for
1586+
the service. See [Authentication scope](#authentication-scope)
1587+
- `state` - security code against false request attacks. It is returned to you by the redeem call,
1588+
where you verify it. See [Protection](#protection)
1589+
- `nonce` - unique parameter which helps to prevent replay attacks. Verify it in the ID token
1590+
- `locale` - user interface language: `et`, `en` or `ru`. `ui_locales` is accepted as an alias
1591+
- `return_url` - the page hosting the widget. Required for the authentication methods that must
1592+
leave the iframe — see [Methods that leave the frame](#methods-that-leave-the-frame). Its origin
1593+
must be on the service's allowlist
1594+
- `theme` - a JSON object styling the widget, see [Theming](#theming)
1595+
1596+
Response fields:
1597+
1598+
- `session_token` - the opaque token the widget is mounted with. It is not a credential of yours
1599+
and is safe to render into the page: it works only from an allowlisted origin
1600+
- `expires_in` - seconds the session remains valid (600 by default). Create a session when the
1601+
user is about to sign in, not on every page render
1602+
- `allowed_methods` - the authentication methods the widget will offer, or `null` for all methods
1603+
configured on the service
1604+
- `locales` - the languages the widget supports
1605+
1606+
## Embedding the widget
1607+
1608+
> Load the SDK and mount the widget
1609+
1610+
```html
1611+
<script src="https://auth.eeid.ee/widget/eeid-widget.js"></script>
1612+
1613+
<eeid-widget
1614+
base-url="https://auth.eeid.ee"
1615+
session-token="gW7s8x1Qm2..."
1616+
height="420px"></eeid-widget>
1617+
```
1618+
1619+
> React to the outcome
1620+
1621+
```javascript
1622+
const widget = document.querySelector("eeid-widget")
1623+
1624+
widget.addEventListener("eeid:success", async (event) => {
1625+
widget.remove() // required, see the note below
1626+
1627+
await fetch("/signin", {
1628+
method: "POST",
1629+
headers: { "Content-Type": "application/json" },
1630+
body: JSON.stringify({ result_token: event.detail.resultToken })
1631+
})
1632+
1633+
location.reload()
1634+
})
1635+
1636+
widget.addEventListener("eeid:error", (e) => showError(e.detail.error))
1637+
widget.addEventListener("eeid:cancel", () => showChooserAgain())
1638+
```
1639+
1640+
The SDK is served by eeID itself — there is nothing to install and no build step. It defines one
1641+
custom element that frames the sign-in form, delegates the browser permissions the framed methods
1642+
need, tracks the iframe height, and re-emits the widget's messages as ordinary DOM events.
1643+
1644+
Attributes of `<eeid-widget>`:
1645+
1646+
- `base-url` (required) - `https://auth.eeid.ee`, or `https://test-auth.eeid.ee` for a service in
1647+
the `Test` environment
1648+
- `session-token` (required) - the token from the create call
1649+
- `height` - initial height, for example `420px`. The widget resizes itself afterwards
1650+
- `theme` - a JSON theme, applied without creating a new session
1651+
- `title` - accessible title of the iframe
1652+
1653+
Method:
1654+
1655+
- `setTheme(partial)` - merges and applies a theme at runtime, for example when your page switches
1656+
to dark mode
1657+
1658+
Events, dispatched on the element with their data in `detail`:
1659+
1660+
- `eeid:ready` - the form is loaded
1661+
- `eeid:resize` - the content height changed; the SDK has already applied it
1662+
- `eeid:success` - authentication finished. `detail.resultToken` is the single-use token to redeem,
1663+
`detail.state` is the `state` you supplied
1664+
- `eeid:error` - authentication failed. `detail.error` and `detail.errorDescription` carry the
1665+
OAuth2 error
1666+
- `eeid:cancel` - the user cancelled
1667+
- `eeid:expand` / `eeid:collapse` - a method asked for the whole viewport (identity verification
1668+
does this). The SDK handles it; listen only if your own layout needs to react
1669+
1670+
<aside class="warning">
1671+
On <code>eeid:success</code> you must remove or hide the iframe yourself. The widget hands over the
1672+
result token and then deliberately stops — it never navigates your page, because that decision is
1673+
yours. If you leave it mounted it keeps showing "Signing you in…", and a completed sign-in looks
1674+
like it hung.
1675+
</aside>
1676+
1677+
## Redeeming the result
1678+
1679+
> Redeem the result token, then exchange the code
1680+
1681+
```shell
1682+
curl -X POST https://auth.eeid.ee/api/embedded/sessions/$RESULT_TOKEN/redeem \
1683+
-u "$EEID_CLIENT_ID:$EEID_CLIENT_SECRET"
1684+
```
1685+
1686+
> Response
1687+
1688+
```json
1689+
{
1690+
"code": "71ed5797c3d957817d31",
1691+
"state": "f3b2c3e7f4cf0bed3a783ed6ece617e3"
1692+
}
1693+
```
1694+
1695+
URL: `POST https://auth.eeid.ee/api/embedded/sessions/{result_token}/redeem`
1696+
1697+
Authenticated with the same client credentials as the create call, and made from your backend.
1698+
1699+
The result token is **single-use and bound to your client**: the first valid call returns the code
1700+
and destroys the session. A replay, an expired token, or a token belonging to another client is
1701+
answered with `410 Gone` and nothing else — an attacker who intercepts a result token cannot learn
1702+
whether it ever existed.
1703+
1704+
Verify that the returned `state` matches the one you sent, then exchange the `code` with an
1705+
[identity token request](#identity-token-request), using the same `redirect_uri` you used when creating the session.
1706+
From that point the flow is identical to the redirect flow, including verification of the ID
1707+
token and its `nonce`.
1708+
1709+
## Methods that leave the frame
1710+
1711+
Smart-ID+ and the cross-border methods (eIDAS, eParaksts, Freja, MojeID) cannot complete inside an
1712+
iframe: a mobile app link is only honoured from a top-level navigation, and a foreign identity
1713+
provider refuses to be framed. For those methods the widget takes over the whole window and then
1714+
returns to your page.
1715+
1716+
That is what `return_url` is for. Pass the URL of the page hosting the widget when you create the
1717+
session. When the user comes back, that URL carries a query parameter:
1718+
1719+
> Resuming after a method that left the frame
1720+
1721+
```php
1722+
<?php
1723+
$sessionToken = $_GET['eeid_session_token'] ?? null;
1724+
1725+
if ($sessionToken === null) {
1726+
$sessionToken = create_session()['session_token'];
1727+
}
1728+
// mount <eeid-widget> with $sessionToken as usual
1729+
```
1730+
1731+
Mount the widget with that token and the flow finishes normally through `eeid:success`. Strip the
1732+
parameter from the address bar as you consume it — it is single-use, so a reload would otherwise
1733+
try to resume a session that is already spent.
1734+
1735+
If you omit `return_url`, those methods fall back to a plain top-level redirect to your
1736+
`redirect_uri?code=…`, exactly like the classic redirect flow.
1737+
1738+
## Theming
1739+
1740+
> A theme covering most needs
1741+
1742+
```json
1743+
{
1744+
"preset": "light",
1745+
"colorScheme": "system",
1746+
"color": { "primary": "#0B5FFF", "onPrimary": "#FFFFFF" },
1747+
"typography": { "fontFamily": "\"Inter\", system-ui, sans-serif" },
1748+
"shape": { "radiusButton": "8px" },
1749+
"branding": { "showFooter": false }
1750+
}
1751+
```
1752+
1753+
The widget is styled with a JSON theme, supplied either as the `theme` parameter when creating the
1754+
session, as the `theme` attribute on the element, or at runtime with `setTheme()` — for example
1755+
when your page switches to dark mode.
1756+
1757+
The main groups are `preset`, `colorScheme` (`light`, `dark`, `system`), `density`
1758+
(`comfortable`, `compact`), `color`, `typography`, `shape`, `spacing`, `components`, `layout`,
1759+
`branding` and `messages`. Unknown keys are rejected rather than ignored, so a typo is reported
1760+
when the session is created instead of silently doing nothing.
1761+
1762+
Note that the first screen the user sees — the list of authentication methods — takes its colours
1763+
from `components.methodList`, not from `color.primary`, which paints buttons. Changing only the
1764+
primary colour therefore looks like nothing happened until a method is selected.
1765+
1766+
For a stylesheet of your own, `advanced.themeCssUrl` loads one extra CSS file inside the widget.
1767+
It must be served from an origin that is already on the service's allowlist.
1768+
1769+
## Errors
1770+
1771+
| Status | Meaning |
1772+
| ------ | ------- |
1773+
| `400 Bad Request` | A parameter is invalid, for example a `return_url` whose origin is not allowlisted |
1774+
| `401 Unauthorized` | The client id or secret is wrong, or the service belongs to a different environment |
1775+
| `403 Forbidden` | Embedded mode is not enabled for this client, or no valid origin is allowlisted |
1776+
| `410 Gone` | The result token is invalid, already redeemed, or expired |
1777+
| `422 Unprocessable Entity` | The theme did not validate, or a required parameter is missing |
1778+
| `429 Too Many Requests` | Rate limited, see below. `Retry-After` gives the seconds to wait |
1779+
| `502 Bad Gateway` | eeID could not reach its authorization server. Retry later |
1780+
1781+
Errors that happen *during* authentication reach your page as an `eeid:error` event rather than as
1782+
a status code — the session was created successfully, the authentication itself failed.
1783+
1784+
## Rate limits
1785+
1786+
The embedded API is rate limited per client and per source address. The limits are set well above
1787+
normal traffic and exist to stop credential guessing: **failed** authentications are counted far
1788+
more strictly than ordinary calls, and a successful call clears that count.
1789+
1790+
If you receive `429 Too Many Requests`, wait the number of seconds given in the `Retry-After`
1791+
header before retrying. Repeated `401`s from a misconfigured secret are the usual way to reach it,
1792+
so fix the credentials rather than retrying in a loop.
1793+
1794+
## Test environment
1795+
1796+
A service in the `Test` environment uses `https://test-auth.eeid.ee` for all three URLs — the
1797+
session API, the widget script and the `base-url` attribute — and the matching token endpoint.
1798+
Test authentications are free. A service exists in exactly one environment at a time, so use the
1799+
host that matches the service your credentials belong to.
1800+
1801+
## Example application
1802+
1803+
A complete integration, small enough to read in one sitting, is published at
1804+
[github.com/internetee/eeid_php_embedded_demo](https://github.com/internetee/eeid_php_embedded_demo).
1805+
1806+
It is a single PHP file plus a `<script>` tag, and it runs the whole loop: creating the session
1807+
server-side, framing the widget, redeeming the result token, exchanging it for the ID token and
1808+
displaying the claims. It also demonstrates theming, locale selection, signing out and signing in
1809+
again, and the resume path for the methods that leave the frame.
1810+
1811+
```shell
1812+
git clone https://github.com/internetee/eeid_php_embedded_demo.git
1813+
cd eeid_php_embedded_demo
1814+
cp .env.example .env # fill in EEID_CLIENT_ID and EEID_CLIENT_SECRET
1815+
docker compose up --build
1816+
```
1817+
1818+
Then open [http://localhost:8081](http://localhost:8081). Remember to add `http://localhost:8081`
1819+
to your service's **Embedded widget allowed origins** first, or the browser will refuse to display
1820+
the widget.
1821+
14641822
# eeID Identification
14651823

14661824
The eeID Identification Service allows organizations to create identification requests and verify user identities based on specific criteria. It is built on a robust framework that ensures security, compliance, and ease of use.

0 commit comments

Comments
 (0)