# Signing In as a Client

https://dev.wisecp.com/es/client-sign-in

Hand a signed-in session to a client from your own system, without ever holding their password.

## Overview

Two endpoints cover the moment a client moves from your system into theirs.

- **POST /clients/validate**: Checks whether an email and password belong to a live client account. Your front end collects the credentials; this tells you which client is behind them.
- **POST /clients/sso**: Issues a one-time sign-in ticket and the URL that spends it. Send the client there and they arrive already signed in.

Use the second one when you already know who the visitor is: your own portal, a control panel, a desk tool. Use the first only when you are the one asking for the password.

## Prerequisites

- **Scopes**: `Clients/ValidateClient` and `Clients/CreateClientSsoToken`. Grant only the one you use.
- **Account state**: The client must be an active member account. Inactive or blocked accounts are refused when the ticket is issued.
- **HTTPS**: The ticket travels in a URL. Serve the redirect over HTTPS so it is not readable in transit.

## Structure

The ticket is a random secret, not a package of data. Everything the sign-in needs — the expiry and the landing page — is stored on the account, so nothing the caller receives can be edited into a different destination or a longer window.

- **Auth::createSsoToken()**: Issues the ticket, stores it encrypted on the account and returns the URL.
- **Auth::verifySsoToken()**: Resolves the ticket and consumes it before any session exists.
- **Auth::ssoLogin()**: Runs the normal sign-in gate, then opens the session.

## Step by Step

### Sending a Client In

1. Your system decides which client this visitor is. Call `POST /clients/sso` with that client's id. You get back a `token`, a `url` and an `expires_at`.
2. Redirect the visitor to `url` straight away. Do not show it as a link for later — the ticket is valid for 60 seconds.
3. The visitor lands signed in, on their dashboard or on the page you named in `destination`. The ticket is spent; the same URL will not work twice.

### Choosing Where They Land

1. Leave `destination` out and the client lands on their dashboard.
2. Send a route key with its parameters to land deeper: `"destination": "services", "destination_values": [128]`.
3. An absolute URL works too, as long as it belongs to this installation. Anything pointing elsewhere is dropped and the client lands on the dashboard instead.

## Reference

### Ticket Request

- **client_id**: Required. The client to issue the ticket for. `user_id` is accepted as its former name.
- **destination**: Optional. A route key or an absolute URL inside this installation.
- **destination_values**: Optional. Route parameters, when `destination` is a route key.

### How the Ticket Behaves

- **Single use**: Spent the moment the URL is opened. A replayed link lands on the sign-in form with an explanatory message.
- **Sixty seconds**: Issue it and follow it in one motion.
- **One per client**: A new ticket silently retires that client's previous one.
- **Sign-in gate**: Account status, country blocking and module vetoes are checked when the ticket is spent, as for a password sign-in.

Full field tables, error codes and response shapes are in the endpoint reference for `clients`.

## Example

Issue a ticket and redirect cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/clients/sso' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"client_id":64,"destination":"services","destination_values":[128]}'
```

```javascript
const res = await fetch('https://panel.example.com/api/v1/admin/clients/sso', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    client_id: 64,
    destination: 'services',
    destination_values: [128],
  }),
});

const body = await res.json();

// Redirect from the server that holds the key, not from the visitor's browser:
// the ticket is a credential and should not pass through untrusted code.
res.ok && console.log(body.data.url);
```

```php
<?php

$ch = curl_init('https://panel.example.com/api/v1/admin/clients/sso');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode(['client_id' => 64]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);

// Send the visitor there now; the ticket expires in 60 seconds.
header('Location: ' . $body['data']['url']);
exit;
```

```php
$response = Api::Clients()->CreateClientSsoToken([
    'client_id'   => 64,
    'destination' => 'services',
    'destination_values' => [128],
]);

// No key, no HTTP round trip — same resource, called in process.
Utility::redirect($response['data']['url']);
```

## Pitfalls

> **A ticket is a credential.** Whoever opens the URL becomes that client. Do not log it, email it or put it in a page a third party can read.

> **Do not mint tickets in advance.** They last 60 seconds and a client holds only one at a time, so issuing a batch leaves you with a single working ticket.

> **Validate does not sign anyone in.** It answers whether a password matches. If you want a session, issue a ticket afterwards.

## Related Articles

- [Client Endpoints](https://dev.wisecp.com/en/client-endpoints)
- [Client Security Endpoints](https://dev.wisecp.com/en/client-security-endpoints)
