Skip to content

Instantly share code, notes, and snippets.

@lionel-rowe
Last active January 30, 2024 07:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lionel-rowe/01b15540e529a4c4d8ce5a2fcaaa454a to your computer and use it in GitHub Desktop.
Save lionel-rowe/01b15540e529a4c4d8ce5a2fcaaa454a to your computer and use it in GitHub Desktop.
// https://dash.deno.com/playground/clearlyloc-phrase-oauth
import { marked } from 'npm:marked@5.0.1'
import { escape } from 'https://deno.land/std@0.208.0/html/entities.ts'
const PHRASE_BASE_URL = 'https://cloud.clearlyloc.com/web/'
const CLIENT_ID = 'WOEVUqZPEgEhblG0eOUsR6'
function authEndpointFromClientId(clientId: string) {
const url = new URL('oauth/authorize', PHRASE_BASE_URL)
url.searchParams.set('client_id', clientId)
url.searchParams.set('response_type', 'code')
return url.href
}
function tokenEndpointFromCode(code: string) {
const url = new URL('oauth/token', PHRASE_BASE_URL)
url.searchParams.set('code', code)
url.searchParams.set('grant_type', 'authorization_code')
return url.href
}
Deno.serve(async (request) => {
const requestUrl = new URL(request.url)
switch (requestUrl.pathname) {
case '/callback': {
const tokenEndpoint = tokenEndpointFromCode(
requestUrl.searchParams.get('code'),
)
const { access_token } = await (await fetch(tokenEndpoint, { method: 'POST' })).json()
const whoAmIUrl = new URL('api2/v1/auth/whoAmI', PHRASE_BASE_URL)
const res = await fetch(whoAmIUrl, {
headers: {
Authorization: `Bearer ${access_token}`,
},
})
if (res.ok) {
return json((await res.json()).user)
} else {
return json(await res.json(), { status: res.status })
}
}
case '/': {
const authEndpoint = authEndpointFromClientId(CLIENT_ID)
return html(marked(instructions(authEndpoint, requestUrl.origin)))
}
default: {
return json({ error: 'Not Found' }, { status: 404 })
}
}
})
function json(body: unknown, { status = 200 }: { status?: number } = {}) {
return new Response(JSON.stringify(body, null, '\t'), {
headers: {
status,
'Content-Type': 'application/json'
}
})
}
function html(body: string, { status = 200 }: { status?: number } = {}) {
return new Response(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Oauth Demo</title><style>:root { font-family: sans-serif; } body { max-width: 80ch; margin: 5rem auto; }</style></head><body>${body}</body></html>`, {
headers: {
status,
'Content-Type': 'text/html'
}
})
}
function instructions(authUrl: URL | string, origin: string) {
const authEndpoint = new URL(authUrl).href
return `# Clearly Local Phrase OAuth Demo
## Demo
<a target="_blank" href="${authEndpoint}">Click here to authorize this application in Phrase</a>.
## Source code
[View source code](https://dash.deno.com/playground/clearlyloc-phrase-oauth).
## Setup
Note: The setup for this demo app has already been completed. Instructions for
this step are just for reference.
1. Set up an application with an endpoint to handle OAuth callbacks from Phrase
(the "redirect URL"). See [§ Source code](#source-code) for example application
logic.
2. Log in to Phrase with an administrator account.
3. Go to https://cloud.clearlyloc.com/web/oauthClient/list.
4. Click "New".
5. Choose a name, redirect URL, and description.
Note: The redirect URL must match the callback endpoint set up in step 1,
i.e. \`https://clearlyloc-phrase-oauth.deno.dev/callback\` for this demo app.
6. Use the client ID to configure the Phrase authorization URL.
## User flow
1. Log in to Phrase with any user account. This doesn't have to be the same as the
account used in setup, nor does it have to be an administrator account.
2. Go to the Phrase
<a target="_blank" href="${authEndpoint}">${abbrUrl(authEndpointFromClientId('<client_id>'))}</a>
endpoint (using the client ID obtained from [§ Setup](#setup)).
3. Click "Allow".
4. Phrase will redirect you to the redirect URL configured in
[§ Setup](#setup)), supplying a \`code\` as a query parameter, i.e.
${abbrUrl('/callback?code=<code>&state=null', origin)}.
5. The application uses the \`code\` from Phrase's callback to get an
\`access_token\`, by making a \`POST\` request to Phrase's
${abbrUrl(tokenEndpointFromCode('<code>'))} endpoint.
It can then use this \`access_token\` as an HTTP header in the format
\`Authorization: Bearer <access_token>\`. This allows the application to
access Phrase with the same permissions as the user account (not the
administrator account used in setup, unless they happen to be the same
account).
In this demo application, we simply use the access token to call Phrase's
\`whoAmI\` endpoint, displaying the authorized user's details.
Note that, upon refreshing the page, we are unable to access the user's
details a second time. This is because the application doesn't store the
user's \`access_token\` anywhere, so a new token is required each time.
To keep a user "logged in", the token must be stored and re-used.
`
}
function abbrUrl(endpoint: string | URL, baseUrl = PHRASE_BASE_URL) {
const base = new URL(baseUrl)
const url = new URL(endpoint, base)
const prefixLength = base.pathname.length
const subPath = url.pathname.slice(prefixLength)
+ url.search.replaceAll(new RegExp(`${encodeURIComponent('<')}([^%]+)${encodeURIComponent('>')}`, 'g'), '<$1>')
return `<abbr style="text-decoration: none; border-bottom: 1px dotted gray;" title="${escape(url.origin + base.pathname + subPath)}"><code>${subPath.replaceAll(/<[^&]+>/g, (m) => `<span style="opacity: 0.5">${escape(m)}</span>`)}</code></abbr>`
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment