-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for rest source auth redirect
- Loading branch information
Showing
3 changed files
with
225 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { SettingsFlow } from "@ory/client" | ||
import type { NextPage } from "next" | ||
import Head from "next/head" | ||
import Link from "next/link" | ||
import { useRouter } from "next/router" | ||
import { ReactNode, useEffect, useState } from "react" | ||
import QRCode from "react-qr-code" | ||
|
||
import { ActionCard, CenterLink, Methods, CardTitle } from "../pkg" | ||
import ory from "../pkg/sdk" | ||
import restSourceClient from "../services/rest-source-client" | ||
|
||
interface Props { | ||
flow?: SettingsFlow | ||
only?: Methods | ||
} | ||
|
||
function AppLoginCard({ children }: Props & { children: ReactNode }) { | ||
return ( | ||
<ActionCard wide className="cardMargin"> | ||
{children} | ||
</ActionCard> | ||
) | ||
} | ||
|
||
const Fitbit: NextPage = () => { | ||
const router = useRouter() | ||
const DefaultHydraUrl = | ||
process.env.HYDRA_PUBLIC_URL || "http://localhost:4444" | ||
const { flow: flowId, return_to: returnTo } = router.query | ||
const [traits, setTraits] = useState<any>() | ||
const [projects, setProjects] = useState<any>([]) | ||
|
||
const handleNavigation = () => { | ||
return restSourceClient.redirectToAuthRequestLink() | ||
} | ||
|
||
|
||
useEffect(() => { | ||
const handleToken = async () => { | ||
if (!router.isReady) return; | ||
|
||
const token = await restSourceClient.getAccessTokenFromRedirect(); | ||
if (token) { | ||
localStorage.setItem("access_token", token); | ||
await restSourceClient.redirectToRestSourceAuthLink(token); | ||
} | ||
}; | ||
|
||
handleToken(); | ||
}, [router.isReady]); | ||
|
||
useEffect(() => { | ||
ory.toSession().then(({ data }) => { | ||
const traits = data?.identity?.traits | ||
setTraits(traits) | ||
setProjects(traits.projects) | ||
}) | ||
}, [flowId, router, router.isReady, returnTo]) | ||
|
||
return ( | ||
<> | ||
<Head> | ||
<title>App Login</title> | ||
<meta name="description" content="NextJS + React + Vercel + Ory" /> | ||
</Head> | ||
<AppLoginCard> | ||
<CardTitle>App Login</CardTitle> | ||
<QrForm | ||
projects={projects} | ||
baseUrl={DefaultHydraUrl} | ||
navigate={handleNavigation} | ||
/> | ||
</AppLoginCard> | ||
<ActionCard wide> | ||
<Link href="/" passHref> | ||
<CenterLink>Go back</CenterLink> | ||
</Link> | ||
</ActionCard> | ||
</> | ||
) | ||
} | ||
|
||
interface QrFormProps { | ||
projects: any[] | ||
baseUrl: string | ||
navigate: any | ||
} | ||
|
||
const QrForm: React.FC<QrFormProps> = ({ projects, baseUrl, navigate }) => { | ||
if (projects) { | ||
return ( | ||
<div className="center"> | ||
{projects.map((project) => ( | ||
<div key={project.id} className="project-form"> | ||
<h3>{project.name}</h3> | ||
<div> | ||
<label className="inputLabel">Connect Your Fitbit</label> | ||
<p>Click the button below to redirect to Fitbit.</p> | ||
<button className="col-xs-4" onClick={navigate}> | ||
Login with Fitbit | ||
</button> | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
) | ||
} else { | ||
return ( | ||
<div className="center"> | ||
<label className="inputLabel">No projects.</label> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
export default Fitbit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
export class RestSourceClient { | ||
private readonly AUTH_BASE_URL = "http://localhost:4444/oauth2" | ||
private readonly GRANT_TYPE = "authorization_code" | ||
private readonly CLIENT_ID = "SEP" | ||
private readonly CLIENT_SECRET = "secret" | ||
private readonly REGISTRATION_ENDPOINT = | ||
"http://localhost:8085/rest-sources/backend/registrations" | ||
private readonly FRONTEND_ENDPOINT = | ||
"http://localhost:8081/rest-sources/authorizer" | ||
|
||
async getAccessToken( | ||
code: string, | ||
redirectUri: string, | ||
): Promise<string | null> { | ||
const bodyParams = new URLSearchParams({ | ||
grant_type: this.GRANT_TYPE, | ||
code, | ||
redirect_uri: redirectUri, | ||
client_id: this.CLIENT_ID, | ||
client_secret: this.CLIENT_SECRET, | ||
}) | ||
|
||
const response = await fetch(`${this.AUTH_BASE_URL}/token`, { | ||
method: "POST", | ||
headers: { "Content-Type": "application/x-www-form-urlencoded" }, | ||
body: bodyParams, | ||
}) | ||
|
||
const data = await response.json() | ||
return data.access_token || null | ||
} | ||
|
||
async getAccessTokenFromRedirect(): Promise<string | null> { | ||
const url = new URL(window.location.href) | ||
const code = url.searchParams.get("code") | ||
if (!code) return null | ||
|
||
const redirectUri = window.location.href.split("?")[0] | ||
return this.getAccessToken(code, redirectUri) | ||
} | ||
|
||
redirectToAuthRequestLink(): void { | ||
const scopes = [ | ||
"SOURCETYPE.READ", | ||
"PROJECT.READ", | ||
"SUBJECT.READ", | ||
"SUBJECT.UPDATE", | ||
"SUBJECT.CREATE", | ||
].join("%20") | ||
|
||
const authUrl = `${this.AUTH_BASE_URL}/auth?client_id=${this.CLIENT_ID}&response_type=code&state=${Date.now()}&audience=res_restAuthorizer&scope=${scopes}&redirect_uri=${window.location.href.split("?")[0]}` | ||
|
||
window.location.href = authUrl | ||
} | ||
|
||
// Make a POST request to the registration endpoint to retrieve the authorization link | ||
async getRestSourceAuthLink(accessToken: string): Promise<string | null> { | ||
const response = await fetch(this.REGISTRATION_ENDPOINT, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
Authorization: `Bearer ${accessToken}`, | ||
}, | ||
body: JSON.stringify({ | ||
userId: "4", | ||
persistent: true, | ||
}), | ||
}) | ||
|
||
const data = await response.json() | ||
if (!data.token || !data.secret) { | ||
console.error("Failed to retrieve auth link") | ||
return null | ||
} | ||
|
||
return `${this.FRONTEND_ENDPOINT}/users:auth?token=${data.token}&secret=${data.secret}` | ||
} | ||
|
||
// Redirect user to the authorization link for the rest source | ||
async redirectToRestSourceAuthLink(accessToken: string): Promise<void> { | ||
const url = await this.getRestSourceAuthLink(accessToken) | ||
if (url) { | ||
console.log("Redirecting to: ", url) | ||
window.location.href = url | ||
} | ||
} | ||
} | ||
|
||
export default new RestSourceClient() |