GitHub App e OAuth ~ Flusso disgiunto

Scritto da Francesco Di Donato • 28 aprile 2022 • 4 minuti di lettura
In un post precedente, abbiamo visto come sia possibile ottenere informazioni dall’API REST di GitHub riguardo a quali repository hanno la nostra App GitHub installata. Questo ci consente di costruire qualcosa di simile al seguente componente.
Componente Import Git Repository di Vercel 🔗
Poiché questi dati sono legati a un’App GitHub, il token necessario è emesso dall’OAuth incluso nell’App GitHub in questione.
Q: Il mio sistema è già basato sull’autenticazione tramite un’App OAuth, non posso modificarlo! Inoltre, è gestito da un terzo (i.e. Stytch 🔗). Tuttavia, il access_token
emesso da un OAuth non è valido per i due endpoint!
A: C’è una soluzione. È leggermente più costosa rispetto a quella precedente, ma funziona.
Indice
- Crea App OAuth e App GitHub
- Autenticati come App GitHub tramite JWT
- Ottieni gli
installation_id
dell’App GitHub - Ottieni gli
access_token
dell’App GitHub - Recupera i repository idonei
- Post correlati
Crea App OAuth e App GitHub
Ovviamente è necessario creare l’App OAuth 🔗 e anche creare l’App GitHub 🔗.
- URL della homepage: http://localhost:3000 🔗
- URL di callback: Dove il provider dovrebbe riportare l’utente una volta che il flusso di autenticazione è completato. Puoi scegliere qualsiasi percorso, sto usando /oauth/github/login/callback
Finalize la creazione. Le è stato assegnato un Client ID ed è possibile generare un Client Secret. Fai questo e tienili a portata di mano.
Simile a quanto spiegato nel post precedente 🔗, puoi ottenere e preservare sul client il access_token
.
server.get("/oauth/github/login/callback", async (request, reply) => {
const { code } = request.query;
const exchangeURL = new URL("login/oauth/access_token", "https://github.com");
exchangeURL.searchParams.set("client_id", process.env.CLIENT_ID);
exchangeURL.searchParams.set("client_secret", process.env.CLIENT_SECRET);
exchangeURL.searchParams.set("code", code);
const response = await axios.post(exchangeURL.toString(), null, {
headers: {
Accept: "application/json",
},
});
const { access_token } = response.data;
const redirectionURL = new URL("new", "http://localhost:3000");
redirectionURL.searchParams.set("access_token", access_token);
reply.status(302).header("Location", redirectionURL).send();
});
Tuttavia, se provi a interrogare i due endpoint che restituiscono le installazioni dell’app e i loro repository, otterrai un 403.
Endpoints
Abbiamo bisogno di un altro tipo di access_token
, uno che sia relativo all’App GitHub.
Quindi, crea l’App GitHub. Questa volta non è davvero necessario assegnare alcun URL di callback (ma potresti voler impostare un URL di configurazione). Ai fini di questo post, sei interessato solo a tenere traccia dell’App ID (sezione Informazioni della tua pagina di configurazione dell’App GitHub) e a generare e memorizzare sul tuo fs la Chiave Privata (in fondo alla pagina).
Autenticati come App GitHub tramite JWT
Come spiega la documentazione ufficiale 🔗:
Autenticarsi come App GitHub ti consente di fare alcune cose […] Puoi richiedere token di accesso per un’installazione dell’app. Per autenticarti come App GitHub, genera una chiave privata in formato PEM e scaricala sul tuo computer. Utilizzerai questa chiave per firmare un JSON Web Token (JWT 🔗) e codificarlo utilizzando l’algoritmo RS256. GitHub controlla che la richiesta sia autenticata verificando il token con la chiave pubblica memorizzata dell’app.
Quindi, il server potrebbe avere un percorso /repos
che di conseguenza genera il JWT:
const secret = fs.readFileSync(
path.resolve(__dirname, ".private-key.pem"),
"utf-8"
);
server.get("/repos", async (request, reply) => {
const now = Math.floor(Date.now() / 1000) - 60; // non usare solo Date.now()
const payload = jwt.sign(
{
iat: now - 60,
exp: now + 10 * 60,
iss: process.env.APP_ID,
},
secret,
{
algorithm: "RS256",
}
);
// ...
});
E una volta creato il JWT, usalo nell’intestazione di autenticazione preceduto da Bearer
(diversamente dalla maggior parte degli endpoint dell’API REST di GitHub, che richiedono token
).
Ottieni gli installation_id
dell’App GitHub
L’endpoint è /app/installations 🔗:
// ancora in /repos
const installations = await axios.get(
`https://api.github.com/app/installations`,
{
headers: {
Authorization: `Bearer ${payload}`,
},
}
);
Viene restituita una lista composta da elementi come il seguente:
{
"id": 25061467,
"account": {
"login": "<some-username>",
...
},
"repository_selection": "selected",
"access_tokens_url": "https://api.github.com/app/installations/25061467/access_tokens",
"repositories_url": "https://api.github.com/installation/repositories",
},
Abbiamo bisogno di filtrare in tutti gli elementi dove .account.login
è uguale all’account personale dell’utente o a uno delle organizzazioni dell’utente. Puoi recuperare queste informazioni tramite /user/org 🔗, tenendo presente di passare l’access_token
recuperato tramite l’app OAuth.
Fondamentalmente, qualcosa di simile:
const relevantInstallations = installations.data.filter((installation) => {
return currentUserOrganizations.includes(installation.account.login);
});
Ottieni gli access_token
dell’App GitHub
Per ciascuna delle installazioni rilevanti per il nostro utente (e tutte le organizzazioni dell’utente), richiediamo un access_token
autenticato dell’App GitHub tramite l’endpoint /app/installations/:installation_id/access_tokens 🔗:
const promises = relevantInstallations.map((installation) => {
return axios.post(
`https://api.github.com/app/installations/${installation_id}/access_tokens`,
null,
{
headers: {
Authorization: `Bearer ${payload}`,
},
}
);
});
// Parallel
const accessTokens = await axios.all(promises)
Nota: invece di assemblare manualmente l’url, potresti utilizzare l’
installation.access_tokens_url
già predisposta.
Recupera i repository idonei
Itera ciascun access_token
e usalo nell’intestazione di autorizzazione token ${access_token}
all’endpoint /installation/repositories
(installation.repositories_url
):
// Nota: per ogni access_token!
const response = await axios.get(
"https://api.github.com/installation/repositories",
{
headers: {
Authorization: `token ${token}`, // non Bearer
},
}
);
const repositories = response.data.repositories;
Unisci o organizza tutti i repository ricevuti e sei tornato alla situazione del primo post. Ci siamo arrivati attraverso un percorso trasversale, leggermente più faticoso - tuttavia, abbiamo raggiunto l’obiettivo.