Commit dc2bb1a
authored
feat: implement auth (with github oauth) (#8)
This Pull request implement the authentication feature in the project;
using the github oauth, our primary goal is to get and hold users github
accessToken in cookies for performing specific functionality. It is
important to state that this feature does not take store this user's
accessToken to any remote server, this token and any other information
that was retrieved using the token are all saved securely on the users'
end through usage of cookies.
### Changes Made
- Implemented the github oauth callback handler at
`/api/github/oauth/callback` - this handler's main functionality is to
receive github's authorization `code` and `state` to perform either of
the following operations
- Redirect to the path stated in the `state` params with the
authorization `code` concatenated to it using the
`Astro.context.redirect` method
- or If a `redirect=true` value if found in the `state` param, then we
redirect to the the path stated in the `state` params with the
authorization `code` and `redirect=true` value concatenated to it using
`Astro.context.redirect` method
- Implemented the github oauth authorization handler at
`/api/github/oauth/authorization` - this handler is a helper that
primarily exchanges the authorization `code` for `tokens` and returns it
in a json object.
- Created a singleton instance of our github `app` at `lib/octokit/app`
- Added a new `crypto` util function which provides `encrypt` and
`decrypt` helper function has exports; it is intended to be used for
securing the users related `cookies`
- Implemented the `doAuth` action function - this function take the
`Astro` global object as argument and performs the operations stated
below
```js
/**
* Authentication action with GitHub OAuth
* @param {import("astro").AstroGlobal} astroGlobal
*/
export default async function doAuth(astroGlobal) {
const { url: { searchParams }, cookies } = astroGlobal;
const code = searchParams.get("code");
const accessToken = cookies.get("jargons.dev:token", {
decode: value => decrypt(value)
});
/**
* Generate OAuth Url to start authorization flow
* @todo make the `parsedState` data more predictable (order by path,
redirect)
* @todo improvement: store `state` in cookie for later retrieval in
`github/oauth/callback` handler for cleaner url
* @param {{ path?: string, redirect?: boolean }} state
*/
function getAuthUrl(state) {
const parsedState = String(Object.keys(state).map(key => key + ":" +
state[key]).join("|"));
const { url } = app.oauth.getWebFlowAuthorizationUrl({
state: parsedState
});
return url;
}
try {
if (!accessToken && code) {
const response = await GET(astroGlobal);
const responseData = await response.json();
if (responseData.accessToken && responseData.refreshToken) {
cookies.set("jargons.dev:token", responseData.accessToken, {
expires: resolveCookieExpiryDate(responseData.expiresIn),
encode: value => encrypt(value)
});
cookies.set("jargons.dev:refresh-token", responseData.refreshToken, {
expires: resolveCookieExpiryDate(responseData.refreshTokenExpiresIn),
encode: value => encrypt(value)
});
}
}
const userOctokit = await app.oauth.getUserOctokit({ token:
accessToken.value });
const { data } = await userOctokit.request("GET /user");
return {
getAuthUrl,
isAuthed: true,
authedData: data
}
} catch (error) {
return {
getAuthUrl,
isAuthed: false,
authedData: null
}
}
}
```
- it provides (in its returned object) a helper function that can be
used to generate a new github oauth url, this helper consumes our github
`app` instance and it accepts a `state` object with `path and `redirect`
property to build out the `state` value that is held within the oauth
url
- it sets `cookies` data for `tokens` - it does this when it detects the
presence of the authorization `code` in the `Astro.url.searchParams` and
reads the absense no project related `accessToken` in cookie; this
assumes that there's a new oauth flow going through it; It performs this
operation by first calling the github oauth authorization handler at
`/api/github/oauth/authorization` where it gets the `tokens` data that
it adds to `cookie` and ensure its securely store by running the
`encrypt` helper to encode it value
- In cases where there's no authorization `code` in the
`Astro.url.searchParams` and finds a project related `token` in
`cookie`, It fetches users's data and provides it in its returned object
for consumptions; it does this by getting the users octokit instance
from our github `app` instance using the `getUserOctokit` method and the
user's neccesasry `tokens` present in cookie; this users octokit
instance is then used to request for user's data which is in turn
returned
- It also returns a boolean `isAuthed` property that can be used to
determine whether a user is authenticated; this property is a statically
computed property that only always returns turn when all operation
reaches final execution point in the `try` block of the `doAuth` action
function and it returns false when an `error` occurs anywhere in the
operation to trigger the `catch` block of the `doAuth` action function
- Added the `login` page which stands as place where where unauthorised
users witll be redirected to; this page integrates the `doAuth` action,
destruing out the `getAuthUrl` helper and the `isAuthed` property, it
uses them as follows
```js
const { getAuthUrl, isAuthed } = await doAuth(Astro);
if (isAuthed) return redirect(searchParams.get("redirect"));
const authUrl = getAuthUrl({
path: searchParams.get("redirect"),
redirect: true
});
```
- `isAuthed` - this property is check on the server-side on the page to
check if a user is already authenticated from within the `doAuth` and
redirects to the value stated the page's
`Astro.url.searchParams.get("redirect")`
- When a user is not authenticated, it uses the `getAuthUrl` to generate
a new github oauth url and imperatively set the argument
`state.redirect` to `true`
- Implemented a new `user` store with a Map store value `$userData` to
store user data to state
### Integration Demo: Protect `/sandbox` page
```js
// pages/sandbox.astro
---
import BaseLayout from "../layouts/base.astro";
import doAuth from "../lib/actions/do-auth.js";
import { $userData } from "../stores/user.js";
const { url: { pathname }, redirect } = Astro;
const { isAuthed, authedData } = await doAuth(Astro);
if (!isAuthed) return redirect(`/login?redirect=${pathname}`);
$userData.set(authedData);
---
<BaseLayout pageTitle="Dictionary">
<main class="flex flex-col max-w-screen-lg p-5 justify-center mx-auto min-h-screen">
<div class="w-fit p-4 ring-2 rounded-full ring-gray-500 m-auto flex items-center space-x-3">
<img class="w-10 h-10 p-1 rounded-full ring-2 ring-gray-500"
src={authedData.avatar_url}
alt={authedData.login}
>
<p>Hello, { authedData.login }</p>
</div>
</main>
</BaseLayout>
```
Explainer
- We destructure `isAuthed` and `authedData` from the `doAuth` action
- Check whether a user is not authenticated? and do a redirect to
`login` page stating the current `pathname` as value for the `redirect`
search param (a data used in state to dictate where to redirect to after
authentication complete) if no user is authenticated
- or Proceed to consuming the `authedData` which will be available when
a `isAuthed` is `true`. by setting it to the `$userData` map store
property
### Screencast/Screenshot
[screencast-bpconcjcammlapcogcnnelfmaeghhagj-2024.03.29-20_36_15.webm](https://github.com/babblebey/jargons.dev/assets/25631971/4063a038-bcff-4e19-91ba-6561c8880410)
### Note
- Added new node package https://www.npmjs.com/package/@astrojs/node for
SSR adapter intergation1 parent a6dd418 commit dc2bb1a
12 files changed
Lines changed: 407 additions & 1 deletion
File tree
- src
- lib
- actions
- octokit
- utils
- pages
- api/github/oauth
- stores
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
8 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
9 | 14 | | |
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
0 commit comments