Skip to content

Commit

Permalink
Add access revokation capability
Browse files Browse the repository at this point in the history
Signed-off-by: Dani Llewellyn <[email protected]>
  • Loading branch information
lucyllewy committed Oct 28, 2022
1 parent 24619f4 commit 9db4dad
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 11 deletions.
37 changes: 36 additions & 1 deletion api/mastodon.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,46 @@ export async function mastodonAuth(request, reply) {
.setCookie(mastodonTokenCookieName, json.access_token, {
maxAge: 3600,
signed: true,
sameSite: 'lax',
sameSite: 'strict',
})
.redirect('/')
}

export async function mastodonDeAuth(request, reply) {
const tokenCookie = request.unsignCookie(request.cookies[mastodonTokenCookieName])
const hostCookie = request.unsignCookie(request.cookies[mastodonHostCookieName])
if (!tokenCookie.valid) {
throw new Error('No Mastodon Authorization Token cookie')
}
if (!hostCookie.valid) {
throw new Error('No Mastodon Hostname cookie')
}

const {client_id, client_secret} = await getMastodonApp.call(this, mastodonDomain)

if (!client_id || !client_secret) {
throw new Error('Where are my credentials?!')
}

const body = `client_id=${encodeURIComponent(client_id)}` +
`&client_secret=${encodeURIComponent(client_secret)}` +
`&token=${encodeURIComponent(tokenCookie.value)}`

const res = await fetch(`${hostCookie.value}/oauth/revoke`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body,
})

if (res.status !== 200) {
return reply.status(res.status).send()
}

reply.clearCookie(mastodonTokenCookieName).send()
}

export async function meHandler(host, token) {
const url = `${host}/api/v1/accounts/verify_credentials`
const response = await fetch(url, {
Expand Down
36 changes: 34 additions & 2 deletions api/twitter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,43 @@ export async function twitterAuth(request, reply) {
.setCookie(twitterTokenCookieName, json.access_token, {
maxAge: json.expires_in,
signed: true,
sameSite: 'lax',
sameSite: 'strict',
})
.redirect('/')
}

/**
* @param {import('fastify').FastifyRequest} request
* @param {import('fastify').FastifyReply} reply
* @returns void
*/
export async function twitterDeAuth(request, reply) {
const token = request.unsignCookie(request.cookies[twitterTokenCookieName])
if (!token.valid) {
throw new Error('No Twitter Authorization Token cookie')
}

const body = `token=${encodeURIComponent(token.value)}` +
'&token_type_hint=access_token' +
`&client_id=${encodeURIComponent(client_id)}`

const res = await fetch('https://api.twitter.com/2/oauth2/revoke', {
method: 'POST',
body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})

const text = await res.text()

if (res.status !== 200) {
return reply.code(res.status).send(text)
}

reply.clearCookie(twitterTokenCookieName).send()
}

export async function meHandler(token) {
const data = await fetch('https://api.twitter.com/2/users/me', {
headers: {
Expand All @@ -80,7 +112,7 @@ export async function meHandler(token) {
if (data.status !== 200) {
throw new Error('Twitter API Error')
}
return data.json()
return await data.json()
}

/**
Expand Down
12 changes: 8 additions & 4 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import servestatic from '@fastify/static'
import {
twitterLoginUrl,
twitterAuth,
twitterDeAuth,
twitterMe,
followingOnTwitter,
} from './api/twitter.mjs'

import {
mastodonLoginUrl,
mastodonAuth,
mastodonDeAuth,
mastodonMe,
} from './api/mastodon.mjs'

Expand All @@ -34,10 +36,10 @@ const app = Fastify({
})

app.register(mongodb, {
// force to close the mongodb connection when app stopped
// the default value is false
forceClose: true,
url: process.env.MongoUrl,
// force to close the mongodb connection when app stopped
// the default value is false
forceClose: true,
url: process.env.MongoUrl,
})

app.register(cookie, {
Expand All @@ -55,9 +57,11 @@ app.register(servestatic, {

app.get('/twitterLoginUrl', twitterLoginUrl)
app.get('/twitterAuth', twitterAuth)
app.get('/twitterDeAuth', twitterDeAuth)
app.get('/twitterMe', twitterMe)
app.get('/mastodonLoginUrl', mastodonLoginUrl)
app.get('/mastodonAuth', mastodonAuth)
app.get('/mastodonDeAuth', mastodonDeAuth)
app.get('/mastodonMe', mastodonMe)
app.get('/followingOnTwitter', followingOnTwitter)
app.get('/addOrUpdateTwitterToMastodonMapping', addOrUpdateTwitterToMastodonMapping)
Expand Down
32 changes: 28 additions & 4 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
max-width: 640px;
}

details > summary {
cursor: pointer;
}

.centered {
text-align: center;
}
Expand All @@ -48,8 +52,8 @@ <h2>Find your Twitter friends on Mastodon</h2>
<iframe src="https://github.com/sponsors/diddledani/button" title="Sponsor diddledani" height="35" width="116" style="border: 0;"></iframe>
</div>
<div>
<p id="twitterLoginParagraph">Step 1. <a id="twitterLoginLink">Login With Twitter</a>.</p>
<p id="mastodonLoginParagraph">Step 2. <span id="mastodonLoginText">Login With Mastodon</span>.</p>
<p id="twitterLoginParagraph">Step 1. <a id="twitterLoginLink">Login With Twitter</a>. <button id="twitterLogoutLink" style="display: none">Logout and revoke access</button></p>
<p id="mastodonLoginParagraph">Step 2. <span id="mastodonLogoutLink">Login With Mastodon</span>. <button id="mastodonLogoutText" style="display: none">Logout and revoke access</button></p>
<p id="mastodonFormParagraph">Enter your Mastodon host's web address:</p>
<form method="get" id="mastodonForm">
<input type="text" placeholder="https://mastodon.social/" id="mastodonHostField" />
Expand All @@ -59,6 +63,7 @@ <h2>Find your Twitter friends on Mastodon</h2>
<p>Step 3. Download matching users in CSV format to import into your Mastodon account. <a style="display: none" id="csvLink" href="/downloadFollowableCSV">Click Here to download the CSV file</a></p>

<p>Important information about how your data is handled is available in the <a href="privacy.html">Privacy Policy</a>.</p>
<p>To revoke access, use the buttons that appear when you are successfully logged into Twitodon. Your login session has a time limit, though, so the buttons will not appear until you refresh your session by logging in again. When visible, clicking the Revoke button will completely dissacotiate Twitodon from your Twitter or Masotodon account.</p>

<details>
<summary>Instructions for importing the CSV into your Mastodon instance</summary>
Expand Down Expand Up @@ -86,6 +91,23 @@ <h2>Find your Twitter friends on Mastodon</h2>
mastodonLoginButton = document.getElementById('loginToMastodon')

async function onLoad() {
document.getElementById('twitterLogoutLink').addEventListener('click', async () => {
const res = await fetch('/twitterDeAuth')
if (res.status === 200) {
window.location.reload()
} else {
alert('Failed to revoke access')
}
})
document.getElementById('mastodonLogoutLink').addEventListener('click', async () => {
const res = await fetch('/mastodonDeAuth')
if (res.status === 200) {
window.location.reload()
} else {
alert('Failed to revoke access')
}
})

document.getElementById('mastodonForm').addEventListener('submit', async e => {
e.preventDefault()
if (mastodonHostField.value && !mastodonLoginButton.hasAttribute('disabled')) {
Expand All @@ -110,9 +132,10 @@ <h2>Find your Twitter friends on Mastodon</h2>
const response = await fetch('twitterMe')
if (response.ok) {
twitterMe = await response.text()
document.getElementById('twitterLoginParagraph').append(` Done. (Logged in as: @${twitterMe})`)
document.getElementById('twitterLoginLink').after(` Done. (Logged in as: @${twitterMe})`)
document.getElementById('twitterLoginLink').style.textDecoration = 'line-through'
document.getElementById('loginToMastodon').removeAttribute('disabled')
document.getElementById('twitterLogoutLink').style.display = 'inline'
} else {
twitterToken = null
}
Expand All @@ -139,7 +162,8 @@ <h2>Find your Twitter friends on Mastodon</h2>
document.getElementById('mastodonForm').style.display = 'none'
document.getElementById('mastodonFormParagraph').style.display = 'none'
document.getElementById('mastodonLoginText').style.textDecoration = 'line-through'
document.getElementById('mastodonLoginParagraph').append(` Done. (Logged in as: @${mastodonMe}@${mastodonHost})`)
document.getElementById('mastodonLoginText').after(` Done. (Logged in as: @${mastodonMe}@${mastodonHost})`)
document.getElementById('mastodonLogoutLink').style.display = 'inline'

const mastodonImportUrl = `https://${mastodonHost}/settings/import`
document.getElementById('mastodonImportLink').textContent = mastodonImportUrl
Expand Down

0 comments on commit 9db4dad

Please sign in to comment.