Last active
July 27, 2023 06:13
-
-
Save ryonakae/fa1432a0826f53830ff25bd1547bce4e to your computer and use it in GitHub Desktop.
TmblrにOAuth 1.0aでログインするCustom Hook (React Native + Expo)
This file contains hidden or 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
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