Saltar al contenido

Crear paneles

Iniciar sesión con Deska

Deja que las personas inicien sesión en el servidor de tu panel con su cuenta de Deska: qué declarar, las dos llamadas, cómo revisar el token en tu servidor, los tokens de desarrollo, los eventos que Deska envía cuando alguien lo detiene y lo que les debes a quienes inician sesión.

Cómo funciona

Un panel con un servidor propio (un tablero compartido, una lista sincronizada) puede iniciar la sesión de las personas con la cuenta de Deska que ya usan, como un sitio web que te deja entrar con otra cuenta. El panel nunca dibuja un formulario de inicio de sesión:

  1. La persona pulsa un botón en tu panel, y la página llama a deska.account.signIn().
  2. Deska muestra su propia hoja, fuera de tu panel: Inicia sesión en <panel> con Deska, un enlace a tu política de privacidad y lo que recibes. La persona pulsa Continuar.
  3. La página recibe un token firmado por deska.dev y lo envía a tu servidor.
  4. Tu servidor revisa el token y luego busca o crea la cuenta de la persona por su sub.

Siempre recibes un id que es de la persona solo para tu panel: dos paneles nunca reciben el mismo id para una persona, así que nunca pueden cruzar a su gente. Recibes su nombre y su correo solo cuando los pides y la persona los marca; los dos empiezan sin marcar.

AtenciónNunca dibujes un formulario de contraseña ni una página de inicio de sesión dentro de un panel, ni para Deska ni para ningún otro servicio. Ninguna función del navegador impide que una página dibuje uno, así que la revisión de Deska rechaza una versión que lo haga. Inicia la sesión de las personas con Deska.

Qué declarar

panel.jsonjson
{
  "permissions": ["account"],
  "network": ["api.nook.app"],
  "account": {
    "privacy": "https://nook.app/privacy",
    "events": "https://api.nook.app/deska/events"
  },
  "sdk": 4
}
  • El permiso account y un objeto account, y cada uno necesita al otro.
  • privacy, obligatorio: la dirección https de tu política de privacidad. La hoja enlaza a ella.
  • events, opcional: una dirección https donde deska.dev avisa a tu servidor de las personas que lo detuvieron. Tiene que estar en uno de tus hosts de network. Consulta Cuando alguien lo detiene.
  • Al menos un host en network: tu servidor, adonde la página envía el token.
  • "sdk": 4. Un Deska más antiguo dice que el panel necesita un Deska más nuevo.
  • Cada dirección es https sobre un nombre de host, sin puerto, usuario ni contraseña, y de 2,048 caracteres como máximo.
  • Un panel que inicia sesiones no puede trabajar además con el agente de una tarea (agent). Consulta Acciones de paneles.

account se pregunta la primera vez que el panel lo usa, nunca al instalar: la pregunta de instalación lo muestra bajo Pregunta primero. Una versión que lo declara siempre se revisa antes de publicarse. deska.dev firma un token solo para una versión publicada de un panel que la cuenta de la persona tiene instalado, nunca para una versión que espera revisión y nunca para una cuenta borrada. Hasta que tu versión esté aprobada, inicia sesión desde una carpeta cargada con tokens de desarrollo.

Iniciar la sesión de alguien

app.jsjs
const API = 'https://api.nook.app'

// Cada pedido lleva un token nuevo; tu servidor lo revisa cada vez
async function call(path, body) {
  const { token } = await deska.account.token()
  const res = await fetch(API + path, {
    method: 'POST',
    headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
    body: JSON.stringify(body),
  })
  if (!res.ok) throw new Error(`El servidor respondió ${res.status}`)
  return res.json()
}

signInButton.addEventListener('click', async () => {
  try {
    await deska.account.signIn({ email: true })
    showBoard(await call('/session', {}))
  } catch (err) {
    if (err.code === 'declined') return // cerró la hoja: Deska vuelve a preguntar cuando regrese al panel
    if (err.code === 'not_signed_in') say('Inicia sesión en Deska y vuelve a intentarlo.')
    else if (err.code === 'permission_denied') say('Está apagado en la página de este panel en Deska.')
    else say('No se pudo iniciar sesión ahora.')
  }
})

// Después de recargar: sin hoja. token() responde permission_denied cuando la cuenta
// de Deska con sesión iniciada nunca pulsó Continuar para este panel en esta computadora
const init = await deska.ready()
try {
  if (!init.permissions.includes('account')) throw new Error('Todavía no permitido')
  showBoard(await call('/session', {}))
} catch {
  showSignIn()
}

deska.on('account', ({ signedIn }) => {
  if (!signedIn) showSignIn() // lo detuvo desde Deska o desde deska.dev
})

deska.account.signIn({ name, email })

Llámalo directamente desde una pulsación en tu panel. La primera vez, Deska pregunta con su hoja, con Continuar activo después de medio segundo. Cuando la persona ya lo permitió, signIn responde al momento, sin hoja, salvo que pida un nombre o un correo que todavía no comparte: entonces una hoja más pequeña, ¿Compartir más con <panel>?, pide solo eso, y Cancelar ahí responde con lo que ya compartía.

Cancelar no guarda nada, pero Deska no muestra una hoja nueva hasta que la persona sale de tu panel y vuelve a él (un clic en él, volver a Deska). Hasta entonces, signIn responde como respondió ese Cancelar, sin hoja: declined, o en una pregunta Compartir más el token con lo que ya compartía. Así una página nunca puede volver a preguntar bajo las siguientes teclas de la persona. La persona hace que un panel deje de preguntar apagando account en su página de la Biblioteca de paneles; desde entonces las dos llamadas responden permission_denied sin preguntar.

Permitir se guarda para la instalación, pero un inicio de sesión pertenece a una cuenta de Deska. Otra cuenta de Deska con sesión iniciada en la misma computadora recibe su propia primera hoja con signIn, y token() le responde permission_denied hasta que pulse Continuar.

Deska le avisa a deska.dev cuando la persona pulsó Continuar, y solo eso inicia una sesión o vuelve a iniciar una que se detuvo. Tu página no hace nada para eso.

Cuando Continuar no puede terminar, la hoja sigue abierta y dice por qué: deska.dev no pudo firmar en ese momento o no se pudo conectar, demasiados inicios de sesión, la lista de instalaciones de la cuenta en deska.dev todavía no tiene el panel (con Reintentar), la versión no es una para la que deska.dev firme, la cuenta de Deska fue eliminada, o la cuenta de Deska cambió mientras Deska preguntaba, así que no se guardó nada. Tu llamada responde host_error cuando la persona cierra la hoja.

ArgumentoTipoNotas
namebooleanOpcional. Pide el nombre de la persona. La hoja ofrece una casilla para él, sin marcar.
emailbooleanOpcional. Pide el correo de la persona, de la misma forma.
Permiso
account
Devuelve
{ token, expiresAt, sub, name?, email? }: el token, cuándo vence en milisegundos desde el epoch, el id de la persona para tu panel, y el nombre y el correo solo cuando la persona los marcó.
Límites
  • 30 tokens por minuto por cuenta, sumando todos los paneles.
ErrorCuándo
not_focusedEl panel no tiene el foco. Llámalo desde una pulsación.
declinedLa persona pulsó Cancelar o cerró la hoja, u otra de las preguntas de Deska está abierta. Después de Cancelar, lo mismo hasta que la persona salga del panel y vuelva a él.
permission_deniedpanel.json no pide account, o la persona lo apagó en la página del panel o lo detuvo.
not_signed_inDeska mismo no tiene la sesión iniciada.
host_errordeska.dev no pudo firmar un token para esta versión, la lista de instalaciones de la cuenta en deska.dev todavía no tiene el panel, la cuenta de Deska fue eliminada, o la cuenta de Deska cambió mientras Deska preguntaba. La hoja dijo por qué antes de que la persona la cerrara.

Pedir un token nuevo

deska.account.token()

Nunca le pregunta a la persona. Un token dura diez minutos, y Deska devuelve el mismo mientras le quede más de un minuto, así que llama a token() antes de cada pedido a tu servidor en vez de guardar uno. Nunca guardes un token en deska.storage.

Permiso
account
Devuelve
La misma forma que signIn, con lo que la persona comparte, válido al menos un minuto más.
ErrorCuándo
permission_deniedLa persona todavía no lo permitió, lo apagó o lo detuvo (aquí, en otra computadora o en deska.dev), o la cuenta de Deska con sesión iniciada nunca pulsó Continuar para este panel en esta computadora.
not_signed_inDeska mismo no tiene la sesión iniciada.
rate_limitedMás de 30 tokens en este minuto para la cuenta.
host_errordeska.dev no pudo firmar en este momento.

Cuando la persona ya lo permitió, init.permissions incluye account, así que una página que se recarga sabe que puede pedir un token sin una pulsación. Esa es la respuesta de la instalación: con otra cuenta de Deska, token() igual puede responder permission_denied, así que muestra entonces tu botón de iniciar sesión. Cuando la persona lo detiene, desde la página del panel en Deska o en deska.dev, cada copia abierta del panel recibe account con { signedIn: false }; recibe { signedIn: true } cuando vuelve a iniciar sesión.

Si la persona lo detiene en otra computadora o en deska.dev, esta computadora se entera cuando Deska vuelve a revisar los inicios de sesión de la cuenta, o cuando deska.dev rechaza la siguiente renovación de un token que pide Deska. En los dos casos Deska lo detiene aquí: token() responde permission_denied y tu página recibe account { signedIn: false }.

Revisar el token en tu servidor

El token es un JWT firmado con ES256. Sus claves públicas están en https://deska.dev/.well-known/jwks.json, y cualquier biblioteca de JWT lo revisa. Este ejemplo usa jose en Node:

server.jsjs
// npm install jose
import { createRemoteJWKSet, jwtVerify } from 'jose'

const JWKS = createRemoteJWKSet(new URL('https://deska.dev/.well-known/jwks.json'))
// El id de tu paquete en producción; la clave dev de tu carpeta cargada mientras construyes
const AUDIENCE = process.env.DESKA_AUDIENCE ?? 'nook'
const PRODUCTION = process.env.NODE_ENV === 'production'

export async function verifyDeskaToken(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: 'https://deska.dev',
    audience: AUDIENCE,
    algorithms: ['ES256'],
    typ: 'JWT', // un token de inicio de sesión; un evento (secevent+jwt) se rechaza aquí
    requiredClaims: ['sub', 'exp'], // exp tiene que estar, y jose lo rechaza cuando ya pasó
  })
  if (payload.dev === true && PRODUCTION) throw new Error('Un token de desarrollo')
  return payload // payload.sub es el id de la persona para tu panel
}

// Express: POST /session con "authorization: Bearer <token>"
app.post('/session', async (req, res) => {
  try {
    const claims = await verifyDeskaToken(req.get('authorization')?.replace(/^Bearer /, ''))
    const account = await findOrCreateAccount(claims.sub) // tu propio código, por sub
    res.json({ account: account.id })
  } catch {
    res.status(401).json({ error: 'not_signed_in' })
  }
})
ClaimQué contiene
isshttps://deska.dev
audEl id de tu paquete. Rechaza cualquier otro: un token para otro panel no es para ti.
subEl id de la persona para tu panel. Nunca cambia para esa persona y ese panel.
iatCuándo lo firmó deska.dev, en segundos.
expDiez minutos después de iat.
jtiÚnico para cada token.
nameSolo cuando la persona compartió su nombre.
email, email_verifiedSolo cuando la persona compartió su correo.
devSolo en un token de desarrollo: true.
  • Revisa la firma con el conjunto de claves según el kid del token, el algoritmo ES256, que el typ del encabezado sea JWT, iss, que aud sea el id de tu paquete, y que exp esté y no haya pasado. jose hace cada una de estas cosas con las opciones de arriba. Revisar typ impide que un evento, que deska.dev firma con las mismas claves, pase alguna vez como un inicio de sesión.
  • Usa sub como clave de la cuenta de la persona, nunca el correo: el correo puede cambiar, y solo lo recibes cuando lo comparte.
  • El origen de tu página es opaco, así que tu servidor tiene que responder CORS para cualquier origen (Access-Control-Allow-Origin: *, y permitir el encabezado authorization). Envía el token en un encabezado, no en una cookie: un panel no guarda cookies.
  • Las claves rotan. deska.dev publica la siguiente clave en el conjunto al menos una hora antes de firmar con ella, y mantiene la anterior ahí al menos una hora después. Busca las claves por kid, y vuelve a pedir el conjunto cuando un token nombra un kid que no tienes (el createRemoteJWKSet de jose lo hace), y nunca pegues una clave en tu código.

Tokens de desarrollo

Una carpeta que cargas en Crear un panel inicia sesión con tokens de desarrollo. Su aud es la clave propia de la carpeta, dev- y doce caracteres hexadecimales, nunca el id que dice su panel.json, y llevan "dev": true. Así, una carpeta que copia el id de un panel real nunca puede iniciar la sesión de nadie en el servidor de ese panel.

  • La clave es init.panel.id en la carpeta cargada, y se mantiene igual mientras la carpeta no se mueva. Acéptala como audiencia solo en tu servidor de desarrollo.
  • Rechaza un token con dev: true en producción, como hace el ejemplo de arriba.
  • deska.dev no guarda registro de los inicios de sesión de desarrollo ni envía eventos por ellos.
  • Una página en localhost (tu servidor de desarrollo) no puede iniciar sesión: no tiene una clave para la que deska.dev firme. Carga la carpeta.

Cuando alguien lo detiene

Una persona deja de iniciar sesión en tu panel desde su página en Deska o desde Paneles en los que iniciaste sesión en su página de cuenta de deska.dev. Cuando lo hace, o cuando borra su cuenta de Deska, deska.dev envía un evento a la dirección events de la última versión que pidió un token:

  • Un POST por HTTPS cuyo cuerpo entero es un Security Event Token (RFC 8417), un JWT con Content-Type: application/jwt, firmado con las mismas claves, cuyo typ en el encabezado es secevent+jwt. Acepta un evento solo con ese typ y un exp que no haya pasado, y nunca lo tomes como un inicio de sesión.
  • Sus claims: iss, aud (el id de tu paquete), iat, exp (cinco minutos después de iat), jti y events. No hay un sub arriba: el id de la persona va dentro del evento.
  • events tiene una sola clave. https://deska.dev/events/panel-sign-in/revoked quiere decir que dejó de iniciar sesión en tu panel. https://deska.dev/events/panel-sign-in/account-deleted quiere decir que borró su cuenta de Deska; llega a cada panel en el que alguna vez inició sesión, también a los que ya detuvo. Cualquiera de las dos es { "sub": "<su id para tu panel>" }.
  • Una respuesta 2xx lo entrega. deska.dev no sigue redirecciones, espera 10 segundos como máximo y vuelve a intentar con una espera que crece, de un minuto hasta cuatro horas, durante 24 horas. Cada intento se firma de nuevo con un iat y un exp nuevos pero el mismo jti: descarta uno que ya viste.
server.jsjs
const REVOKED = 'https://deska.dev/events/panel-sign-in/revoked'
const DELETED = 'https://deska.dev/events/panel-sign-in/account-deleted'

// Express: la dirección "events" de panel.json. Todo el cuerpo es el JWT.
app.post('/deska/events', express.text({ type: 'application/jwt' }), async (req, res) => {
  let event
  try {
    const { payload } = await jwtVerify(req.body, JWKS, {
      issuer: 'https://deska.dev',
      audience: AUDIENCE,
      algorithms: ['ES256'],
      typ: 'secevent+jwt', // un evento, nunca un token de inicio de sesión
      requiredClaims: ['jti', 'exp', 'events'], // exp es cinco minutos después de iat
    })
    event = payload
  } catch {
    return res.sendStatus(400)
  }
  // Tu propio código: markSeen responde false para un jti que ya vio
  if (await markSeen(event.jti)) {
    if (event.events[REVOKED]) await endSessions(event.events[REVOKED].sub)
    if (event.events[DELETED]) await deleteAccount(event.events[DELETED].sub)
  }
  res.sendStatus(204)
})

Con revoked, deja de tratar ese sub como una sesión iniciada. Con account-deleted, borra lo que tu servidor guarda de él.

Lo que les debes a quienes inician sesión

  • Una política de privacidad en la dirección de panel.json que diga qué guarda tu servidor y por qué.
  • Una forma de borrar su cuenta y lo que guardas, desde tu panel o tu sitio, y borrarla cuando llega account-deleted. Detener no es borrar: después de revoked, conserva lo que hizo solo como dice tu política.
  • Pide el nombre o el correo solo cuando el panel los usa, y di por qué junto al botón.
  • Nunca dibujes un formulario de contraseña ni una página de inicio de sesión, y nunca pidas una contraseña ni un código de Deska.
  • Nunca pases el token, el id, el nombre ni el correo a nadie más.

Qué responde cada caso

CasosignIntoken
PermitidoUn token, sin hojaUn token
Todavía sin preguntarLa hoja de Deskapermission_denied
Apagado en la página del panel, o detenidopermission_deniedpermission_denied
Cancelar, Escape o el botón de cerrardeclined, y no se guarda nada. En una pregunta por más: un token con lo que ya compartía. Lo mismo, sin hoja, hasta que la persona sale del panel y vuelve a él.
Permitido para la instalación, pero la cuenta de Deska con sesión iniciada nunca pulsó Continuar aquíLa hoja de Deska, como un primer inicio de sesiónpermission_denied
Otra de las preguntas de Deska está abiertadeclined
El panel no tiene el foconot_focused
Deska no tiene la sesión iniciadanot_signed_innot_signed_in
deska.dev se negó o no pudo firmar (una versión no publicada, un panel que todavía no está en la lista de instalaciones de la cuenta, una cuenta eliminada), o la cuenta de Deska cambió mientras Deska preguntabahost_error, cuando la persona cierra la hoja, que dice por quéhost_error
Detenido en otra computadora o en deska.devpermission_denied, cuando Deska lo detuvo aquípermission_denied