OAuth
Moduł OAuth pozwala odebrać przekierowanie OAuth od zewnętrznego serwisu (Amazon SP-API, Allegro i inne) bez publicznej strony modułu (od SDK 4.2.0).
const { OAuth } = require('@orbito/api-lib');
OAuth.createTicket({ finishMethod, returnTo, userId, meta?, ttl? }) // -> { state, redirectUri, expiresAt }
Dlaczego callback odbiera rdzeń
Dostawcy OAuth rejestrują adres powrotu (redirect URI) raz, globalnie, dla całej aplikacji autora modułu. Adres nie może więc zależeć od firmy ani od instalacji modułu. Moduł działa w izolowanym kontenerze i nigdy nie serwuje odpowiedzi HTTP, więc ten jeden publiczny adres należy do rdzenia Orbito. Rdzeń odpowiada na niego wyłącznie przekierowaniem (302 bez treści) i nie uruchamia przy tym kodu modułu. Parametry callbacku trafiają do modułu dopiero później, w kontekście zalogowanego użytkownika.
Podstawą jest bilet: jednorazowy, powiązany z użytkownikiem i firmą rekord, który moduł zakłada tuż przed wysłaniem użytkownika do dostawcy.
Przepływ
- Użytkownik klika na stronie modułu przycisk „Połącz”. OVE woła metodę modułu (w kontekście użytkownika).
- Metoda tworzy bilet przez
OAuth.createTicketi dostajestateorazredirectUri. Z nich buduje adres zgody dostawcy (redirect_uri,state) i zwraca go do OVE. - Akcja OVE
redirectprzekierowuje przeglądarkę na ten adres. - Dostawca odsyła przeglądarkę na
redirectUrirdzenia. Rdzeń zapisuje wszystkie parametry zapytania na bilecie i przekierowuje przeglądarkę na front Orbito. - Front, z zalogowanym użytkownikiem, kończy bilet. Rdzeń wywołuje metodę
finishMethodinstancji z{ callback, meta, redirectUri }, w kontekście tego użytkownika. - Moduł w
finishMethodwymienia kod na token przezCore.fetchi zapisuje tokeny wConfig. Potem front przechodzi na stronęreturnTo.
Bilet jest jednorazowy: po odebraniu callbacku state jest zużyty, a drugi callback lub drugi finish kończy się błędem. Finish może wykonać tylko użytkownik wskazany przy tworzeniu biletu i tylko w firmie instancji. Chroni to przed podpięciem cudzego konta dostawcy do instalacji innej firmy.
OAuth.createTicket
Tworzy bilet dla bieżącej instancji. Wołaj ją z metody uruchamianej przez użytkownika („Połącz”), tuż przed wysłaniem przeglądarki do dostawcy. Nie wołaj jej przy renderze strony, bo każde wywołanie zużywa bilet. W odróżnieniu od Log, Problems i History metoda odrzuca Promise przy błędzie (np. niepoprawny payload).
const { state, redirectUri, expiresAt } = await OAuth.createTicket(payload);
| Pole | Typ | Opis |
|---|---|---|
finishMethod | string | Nazwa metody tej instancji (zarejestrowanej przez Core.registerMethod), wołanej po powrocie użytkownika od dostawcy, np. oauth.finish |
returnTo | object | Dokąd front przechodzi po finish: { module, path, params? }, jak navigate w OVE. Ustalane przy tworzeniu biletu, callback nie ma na nie wpływu |
returnTo.module | string | Kod własnego modułu albo core dla strony rdzenia. Każdy inny moduł jest odrzucany |
returnTo.path | string | Ścieżka strony w module, np. /settings (1–500 znaków) |
returnTo.params | object | Parametry strony (opcjonalne) |
userId | string | Użytkownik, który może zakończyć bilet (24 znaki hex). Wymagane, przekaż ctx.userId metody tworzącej bilet |
meta | object | Dane zwracane do finishMethod bez zmian (region, sandbox/produkcja itp.), maks. 4 KB po serializacji do JSON. Opcjonalne |
ttl | number | Ile sekund użytkownik ma na zgodę u dostawcy: 60–1800, domyślnie 600 |
Wynik:
| Pole | Typ | Opis |
|---|---|---|
state | string | Losowy identyfikator biletu generowany przez rdzeń. Przekaż go dostawcy w parametrze state |
redirectUri | string | Adres callbacku rdzenia, ten sam, który jest zarejestrowany u dostawcy. Przekaż go jako redirect_uri |
expiresAt | string | Termin ważności biletu (data ISO) |
Pole redirectUri zawsze bierz z wyniku. Nie wpisuj adresu na stałe, dzięki temu ten sam kod działa lokalnie i na produkcji.
Metoda finishMethod
Metoda wskazana w finishMethod jest zwykłą metodą modułu (Core.registerMethod), wołaną przez rdzeń po powrocie użytkownika. Dostaje parametry { callback, meta, redirectUri }:
| Pole | Typ | Opis |
|---|---|---|
callback | object | Wszystkie parametry zapytania, które wysłał dostawca (code, error, error_description i inne, specyficzne dla dostawcy) |
meta | object | Obiekt przekazany do createTicket, bez zmian |
redirectUri | string | Ten sam adres co w bilecie. Większość dostawców wymaga jego powtórzenia przy wymianie kodu na token, więc moduł nie musi go przechowywać między krokami |
Drugi argument, ctx, to zwykły kontekst użytkownika (userId, permissions), czyli tego, który kliknął „Połącz”.
- Odmowa po stronie dostawcy (
?error=access_denied&error_description=...) przychodzi tą samą drogą. Sprawdź najpierwcallback.errori sam zdecyduj, co pokazać. - Rzucony błąd oznacza bilet jako
failed(bilet jest zużyty), a front pokazuje użytkownikowi komunikat z błędu. Pisz go więc tak, by użytkownik go zrozumiał. - Wartość zwrócona przez metodę nie jest używana przez stronę powrotu.
Pełny przykład
Metoda connect tworzy bilet i zwraca adres zgody, a oauth.finish wymienia kod na token:
const { Core, Config, OAuth } = require('@orbito/api-lib');
Core.registerMethod('connect', async (params, ctx) => {
const { state, redirectUri } = await OAuth.createTicket({
finishMethod: 'oauth.finish',
returnTo: { module: 'vendor/shop', path: '/settings' },
userId: ctx.userId,
meta: { region: params.region }
});
const url = new URL('https://provider.example.com/oauth/authorize');
url.searchParams.set('client_id', CLIENT_ID);
url.searchParams.set('response_type', 'code');
url.searchParams.set('redirect_uri', redirectUri);
url.searchParams.set('state', state);
return { url: url.toString() };
});
Core.registerMethod('oauth.finish', async ({ callback, meta, redirectUri }) => {
if (callback.error) {
throw new Error(`Dostawca odmówił dostępu: ${callback.error_description || callback.error}`);
}
const res = await Core.fetch(`https://api.${meta.region}.provider.example.com/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: callback.code,
redirect_uri: redirectUri,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
}).toString()
});
if (res.status !== 200) {
throw new Error(`Wymiana kodu nie powiodła się (${res.status})`);
}
const { access_token, refresh_token, expires_in } = res.json();
await Config.set('runtime.tokens', {
accessToken: access_token,
refreshToken: refresh_token,
expiresAt: Date.now() + expires_in * 1000
});
return { connected: true };
});
Fragment strony OVE z przyciskiem: wynik metody connect ($response.0.url) trafia do akcji redirect:
type: button
title: Połącz
onClick:
- action: callMethod
module: vendor/shop
method: connect
onSuccess:
- action: redirect
url: { valueFrom: $response.0.url }
Biała lista domen
Wymiana kodu na token idzie przez Core.fetch, więc domena endpointu tokenów dostawcy (w przykładzie api.<region>.provider.example.com) musi być na białej liście domen firmy. Sam adres zgody nie wymaga wpisu, bo przeglądarka otwiera go bezpośrednio.
Adres callbacku do rejestracji u dostawcy
Na produkcji createTicket zwraca dziś jeszcze redirectUri o wartości https://v2.orbito.pl/api/oauth/callback. To okres przejściowy: dostawcy mają zarejestrowany stary adres, a ten nadal działa, bo v2.orbito.pl przekierowuje na my.orbi.do. Docelowo będzie to https://my.orbi.do/api/oauth/callback. Jako redirect URI aplikacji u dostawcy rejestruj zawsze dokładnie ten adres, który zwraca createTicket.
Adres zgody zwracany do OVE zawiera & i =, a szablon {{ }} je escapuje. Dlatego w akcji redirect podawaj go przez valueFrom, a nie przez {{ $response.0.url }}.
