Skip to content

Instantly share code, notes, and snippets.

@ryonakae
Last active July 27, 2023 06:13
Show Gist options
  • Save ryonakae/fa1432a0826f53830ff25bd1547bce4e to your computer and use it in GitHub Desktop.
Save ryonakae/fa1432a0826f53830ff25bd1547bce4e to your computer and use it in GitHub Desktop.
TmblrにOAuth 1.0aでログインするCustom Hook (React Native + Expo)
import * as AuthSession from 'expo-auth-session'
import Constants from 'expo-constants'
import * as WebBrowser from 'expo-web-browser'
import qs from 'qs'
import hmacsha1 from 'hmacsha1'
const consumerKey = Constants.expoConfig?.extra?.oauthConsumerKey
const consumerSecret = Constants.expoConfig?.extra?.oauthConsumerSecret
// https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturioptions
const redirectUri = AuthSession.makeRedirectUri({ scheme: 'my-scheme', path: 'redirect' })
export function useTumblrAuthentication() {
function getNonce(length: number) {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
let nonce = ''
for (let i = 0; i < length; ++i) {
nonce += chars.charAt(Math.floor(Math.random() * chars.length))
}
return nonce
}
function getSignature(
url: string,
method: 'GET' | 'POST',
keys: string[],
params: {
[key: string]: any
}
) {
let signatureKey = keys.join('&')
if (keys.length === 1) {
signatureKey = signatureKey + '&'
}
let signatureBase = method + '&' + strictUriEncode(url) + '&'
let queryParameters = ''
Object.keys(params).forEach((key, index) => {
const paramTitle = `${key}`
let paramContent = `${params[key]}`
// oauth_callbackの場合だけstrictUriEncodeする
if (key === 'oauth_callback') {
paramContent = strictUriEncode(paramContent)
}
let parameter = `${paramTitle}=${paramContent}`
if (index !== 0) {
parameter = '&' + parameter
}
queryParameters = queryParameters + parameter
})
signatureBase = signatureBase + strictUriEncode(queryParameters)
const signature = hmacsha1(signatureKey, signatureBase)
return signature as string
}
function getAuthorizationHeader(params: { [key: string]: any }) {
let authorizationHeader = `OAuth `
Object.keys(params).forEach((key, index) => {
let paremeter = params[key]
paremeter = `${key}="${params[key]}"`
if (index !== 0) {
paremeter = ', ' + paremeter
}
authorizationHeader = authorizationHeader + paremeter
})
return authorizationHeader
}
// For RFC 3986 Compliant URI Encoding.
function strictUriEncode(str: string) {
return encodeURIComponent(str)
.replace(/!/g, '%21')
.replace(/'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/\*/g, '%2A')
}
function getNow() {
return Math.floor(new Date().getTime() / 1000)
}
async function signInWithOauth1() {
console.log('auth start')
const requestTokenUrl = 'https://www.tumblr.com/oauth/request_token'
// STEP1: 一時的なoauth_tokenとoauth_token_secretを取得
// https://www.tumblr.com/docs/en/api/v2#temporary-credentials-endpoint
const requestTokenParams = {
oauth_callback: redirectUri,
oauth_consumer_key: consumerKey,
oauth_nonce: getNonce(32),
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp: getNow(),
oauth_version: '1.0'
}
const requestTokenSignature = getSignature(
requestTokenUrl,
'POST',
[consumerSecret],
requestTokenParams
)
const requestTokenAuthorizationHeader = getAuthorizationHeader({
oauth_consumer_key: consumerKey,
oauth_nonce: requestTokenParams.oauth_nonce,
oauth_signature: strictUriEncode(requestTokenSignature),
oauth_signature_method: requestTokenParams.oauth_signature_method,
oauth_timestamp: requestTokenParams.oauth_timestamp,
oauth_version: requestTokenParams.oauth_version
})
const requestTokenResponse = await fetch(
`${requestTokenUrl}?oauth_callback=${strictUriEncode(redirectUri)}`,
{
method: 'POST',
headers: {
Authorization: requestTokenAuthorizationHeader,
'Content-Type': 'application/json'
},
body: JSON.stringify({
oauth_consumer_key: consumerKey,
oauth_nonce: requestTokenParams.oauth_nonce,
oauth_signature: requestTokenSignature,
oauth_signature_method: requestTokenParams.oauth_signature_method,
oauth_timestamp: requestTokenParams.oauth_timestamp,
oauth_version: requestTokenParams.oauth_version
})
}
).catch(err => {
throw new Error(err)
})
const requestTokenResponseText = await requestTokenResponse.text()
if (requestTokenResponse.status !== 200) {
throw new Error(requestTokenResponseText)
}
const parsedRequestTokenResponse = qs.parse(requestTokenResponseText)
// STEP2: 取得したoauth_tokenを使って、認証画面を開く
// https://www.tumblr.com/docs/en/api/v2#resource-owner-authorization-endpoint
const authSessionUrl = `https://www.tumblr.com/oauth/authorize?oauth_token=${parsedRequestTokenResponse.oauth_token}`
const authSessionResult = await WebBrowser.openAuthSessionAsync(
authSessionUrl,
redirectUri
).catch(err => {
throw new Error(err)
})
if (authSessionResult.type !== 'success') {
throw new Error('authSessionResult is not success')
}
const parsedAuthSessionResult = qs.parse(authSessionResult.url.split('?')[1])
// STEP3: oauth_verifierを使って、oauth_tokenとoauth_token_secretを取得
// https://www.tumblr.com/docs/en/api/v2#access-token-endpoint
const accessTokenUrl = 'https://www.tumblr.com/oauth/access_token'
const oauthToken = parsedAuthSessionResult.oauth_token as string
const oauthTokenSecret = parsedRequestTokenResponse.oauth_token_secret as string
// なぜかoauth_verifierは#_=_で終わるので、それを削除する
const oauthVerifier = (parsedAuthSessionResult.oauth_verifier as string).replace('#_=_', '')
const accessTokenParams = {
oauth_consumer_key: consumerKey,
oauth_nonce: getNonce(32),
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp: getNow(),
oauth_token: oauthToken,
oauth_verifier: oauthVerifier,
oauth_version: '1.0'
}
const accessTokenSignature = getSignature(
accessTokenUrl,
'POST',
[consumerSecret, oauthTokenSecret],
accessTokenParams
)
const accessTokenAuthorizationHeader = getAuthorizationHeader({
oauth_consumer_key: consumerKey,
oauth_nonce: accessTokenParams.oauth_nonce,
oauth_signature: strictUriEncode(accessTokenSignature),
oauth_signature_method: accessTokenParams.oauth_signature_method,
oauth_timestamp: accessTokenParams.oauth_timestamp,
oauth_token: accessTokenParams.oauth_token,
oauth_version: accessTokenParams.oauth_version
})
const accessTokenResponse = await fetch(`https://www.tumblr.com/oauth/access_token`, {
method: 'POST',
headers: {
Authorization: accessTokenAuthorizationHeader,
'Content-Type': 'application/json'
},
body: JSON.stringify({
oauth_verifier: oauthVerifier
})
}).catch(err => {
throw new Error(err)
})
if (accessTokenResponse.status !== 200) {
throw new Error(`${accessTokenResponse.status}: ${accessTokenResponse.statusText}`)
}
const accessTokenResponseText = await accessTokenResponse.text().catch(err => {
throw new Error(err)
})
const parsedAccessTokenResponse = qs.parse(accessTokenResponseText)
// 認証成功!!
// 帰ってきたoauth_tokenとoauth_token_secretを使って、好きなようにAPIを叩く
console.log('auth success!!')
console.log('parsedAccessTokenResponse:', parsedAccessTokenResponse)
}
return { signInWithOauth1 }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment