GitHub предлагает бесплатный API для аутентификации пользователей. Он основан на OAuth, открытом стандарте аутентификации. OAuth — довольно обширная тема, но наш случай использования не так уж сложен. Вы можете узнать больше в документации GitHub, но вот как это работает по сути:
- Мы создадим приложение GitHub и введем в него URL обратного вызова. Мы получим идентификатор клиента и секрет клиента (это просто строки случайного вида).
- Мы добавим ссылку «Войти с GitHub» на нашу страницу. Ссылка будет указывать на URL GitHub, который будет включать наш ID клиента и случайную строку, которую мы сгенерируем (называемую «state») в параметрах запроса.
- GitHub покажет пользователю страницу в зависимости от статуса аутентификации:
- Пользователю будет показана страница входа в GitHub (только если он еще не вошел в систему).
- Пользователя спросят, хочет ли он авторизоваться в нашем приложении (только если он еще не авторизовался в нашем приложении).
- Если пользователь согласится (или недавно авторизовался), GitHub перенаправит его на URL обратного вызова, который мы определили в шаге 1.
- Перенаправление будет включать код и состояние, которое мы отправили в шаге 2, в качестве параметров запроса. Если состояние не совпадет со случайной строкой, которую мы отправили, мы поймем, что происходит что-то подозрительное, и прервем процесс. В противном случае мы отправим POST-запрос на
https://github.com/login/oauth/access_token
вместе с нашим идентификатором клиента, секретом клиента и кодом, который мы получили в качестве параметра запроса. Если все пройдет успешно, GitHub ответит нам маркером доступа. - Мы будем использовать маркер доступа в заголовке
Authorization
каждый раз, когда захотим получить данные профиля пользователя от GitHub.
У нас есть план. Давайте начнем.
Создайте приложение GitHub
Перейдите в GitHub Developer Settings, слева нажмите OAuth Apps, а затем нажмите кнопку «New OAuth app». Он задаст вам несколько вопросов. Введите http://localhost:5173
для URL домашней страницы и http://localhost:5173/login
для URL обратного вызова, а остальное заполните по своему усмотрению. Мы указываем адреса localhost
, потому что нам нужно протестировать наше приложение перед развертыванием на его окончательный URL. Вы можете просто обновить URL при развертывании или создать новое приложение, а это оставить для тестирования и разработки.
После отправки формы вы окажетесь на странице, где будет указан идентификатор клиента вашего приложения. Вы также увидите кнопку «Сгенерировать новый клиентский секрет». Сгенерируйте его и скопируйте оба в новый файл в корневом каталоге вашего репозитория и сохраните его с именем .env
. Он должен выглядеть следующим образом:
GITHUB_CLIENT_ID=<your client ID>
GITHUB_CLIENT_SECRET=<your client secret>
Хорошей практикой является сохранение секретов и конфигурации нашего приложения в переменных окружения. Теперь добавьте этот файл в файл .gitignore
, чтобы случайно не опубликовать свой секрет на GitHub. Чтобы загрузить его в окружение во время разработки, мы установим пакет dotenv
:
npm install -D dotenv
Затем мы импортируем его и вызовем в нашем vite.config.ts
. В итоге файл будет выглядеть следующим образом:
import { defineConfig } from "vite";
import rakkas from "rakkasjs/vite-plugin";
import tsconfigPaths from "vite-tsconfig-paths";
dotenv.config();
export default defineConfig({
envDir: ".",
plugins: [
tsconfigPaths(),
rakkas({
adapter: "cloudflare-workers",
}),
],
});
Теперь мы сможем обращаться к переменным, например, process.env.GITHUB_CLIENT_ID
в нашем коде на стороне сервера. process.env
— это глобал, специфичный для Node, но Rakkas делает его доступным и для Cloudflare Workers.
Добавление ссылки «Войти»
Сейчас у нас есть только одна страница. Но так будет не всегда. Вероятно, мы хотим видеть ссылку «Войти» в заголовке каждой страницы. В Rakkas есть система макетов для таких общих элементов. Макеты оборачивают вложенные макеты и страницы в одном каталоге и его подкаталогах. Так, если мы создадим файл layout.tsx
в директории src/routes
, он обернет все страницы нашего приложения.
Мы сказали, что ссылка «Войти» будет указывать на URL-адрес GitHub. Этот URL, согласно документации GitHub, имеет вид https://github.com/login/oauth/authorize?client_id=<CLIENT_ID>${clientId}&state=<STATE>
. ID нашего клиента находится в process.env.GITHUB_CLIENT_ID
, который доступен только на стороне сервера. Поэтому мы снова используем useServerSideQuery
для доступа к нему. Обработкой параметра state
мы займемся позже, а пока присвоим ему 12345
. Итак, вот первый проект нашего src/routes/layout.tsx
:
import { LayoutProps, useServerSideQuery } from "rakkasjs";
export default function MainLayout({ children }: LayoutProps) {
const {
data: { clientId, state },
} = useServerSideQuery(() => ({
clientId: process.env.GITHUB_CLIENT_ID,
state: "12345",
}));
return (
<>
<header>
<strong>uBlog</strong>
<a
style={{ float: "right" }}
href={
"https://github.com/login/oauth/authorize" +
`?client_id=${clientId}` +
`&state=${state}`
}
>
Sign in with GitGub
</a>
<hr />
</header>
{children}
</>
);
}
Когда вы запустите сервер dev, вы увидите, что теперь у нас есть заголовок сайта. А ссылка «Sign in with GitHub» приведет вас на страницу авторизации GitHub. Если вы авторизуете свое приложение, GitHub перенаправит вас на URL, который выглядит как http://localhost:5173/login?code=<BUNCH_OF_RANDOM_LOOKING_CHARS>&state=12345
. http://localhost:5173/login
— это URL, который мы ввели в качестве URL обратного вызова, а остальные — это параметры, отправленные GitHub. Конечно, вы получите ошибку 404, потому что мы еще не реализовали эту конечную точку. Давайте сделаем это сейчас.
Обратный вызов логина
Мы создадим файл src/routes/login.page.tsx
для реализации обратного вызова входа. В нем мы будем использовать параметр запроса code
для получения маркера доступа от GitHub, а затем использовать этот маркер доступа для получения данных профиля пользователя. Мы снова будем использовать хук useServerSideQuery
, потому что не хотим раскрывать наш клиентский секрет клиенту. Помните, что обратный вызов useServerSideQuery
выполняется на сервере и не будет частью клиентского пакета. Давайте сначала посмотрим, как выглядят данные профиля, распечатав их в формате JSON:
import { PageProps, useServerSideQuery } from "rakkasjs";
export default function LoginPage({ url }: PageProps) {
const error = url.searchParams.get("error");
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const { data: userData } = useServerSideQuery(async () => {
if (code && state === "12345") {
const { access_token: token } = await fetch(
"https://github.com/login/oauth/access_token" +
`?client_id=${process.env.GITHUB_CLIENT_ID}` +
`&client_secret=${process.env.GITHUB_CLIENT_SECRET}` +
`&code=${code}`,
{
method: "POST",
headers: { Accept: "application/json" },
}
).then((r) => r.json<{ access_token: string }>());
if (token) {
const userData = fetch("https://api.github.com/user", {
headers: {
Authorization: `token ${token}`,
},
}).then((r) => r.json());
return userData;
}
}
});
if (error) {
return <div>Error: {error}</div>;
}
return <pre>{JSON.stringify(userData, null, 2)}</pre>;
}
Если все идет хорошо, вы должны увидеть данные профиля пользователя GitHub в формате JSON при нажатии на кнопку «Войти с GitHub». Мои данные выглядят следующим образом:
{
"login": "cyco130",
"id": 10846005,
"node_id": "MDQ6VXNlcjEwODQ2MDA1",
"avatar_url": "https://avatars.githubusercontent.com/u/10846005?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/cyco130",
"html_url": "https://github.com/cyco130",
"followers_url": "https://api.github.com/users/cyco130/followers",
"following_url": "https://api.github.com/users/cyco130/following{/other_user}",
"gists_url": "https://api.github.com/users/cyco130/gists{/gist_id}",
"starred_url": "https://api.github.com/users/cyco130/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/cyco130/subscriptions",
"organizations_url": "https://api.github.com/users/cyco130/orgs",
"repos_url": "https://api.github.com/users/cyco130/repos",
"events_url": "https://api.github.com/users/cyco130/events{/privacy}",
"received_events_url": "https://api.github.com/users/cyco130/received_events",
"type": "User",
"site_admin": false,
"name": "Fatih Aygün",
"company": "Lityum AŞ",
"blog": "",
"location": "Istanbul",
"email": null,
"hireable": null,
"bio": "Programmer, musician, amateur linguist.",
"twitter_username": "cyco130",
"public_repos": 32,
"public_gists": 4,
"followers": 26,
"following": 25,
"created_at": "2015-02-04T09:24:28Z",
"updated_at": "2022-06-29T03:02:45Z"
}
Успехов! Мы многого добились! Самое время сделать перерыв!
Что дальше?
В следующей статье мы закончим работу над функцией аутентификации. Мы будем использовать cookies, чтобы помнить, кто есть кто.
Прогресс до этого момента вы можете найти на GitHub.