BlockStack身份授權流程
- 2020 年 4 月 1 日
- 筆記
為什麼需要應用授權
去中心化身份的前提條件,是在同一個身份平台所能覆蓋的範圍內,用戶的身份識別和檢測標準統一,作為區塊鏈應用開發基礎設施的服務提供商,BlockStack 在數據許可權上將應用許可權和用戶身份/數據分離,保障用戶數據所有權。
這種設計雖然實現起來較為複雜,且需要多種類型的服務提供支援,但不論是對用戶,開發者,還是整個 Blockstack 生態,都是非常優雅的方案。

mmexport1585486832221.jpg
- 用戶
- gaia 通過 app 域名隔離數據許可權,無需擔心全量數據安全
- 可以使用多身份來管理相同的應用數據
- 使用應用之前明確的清楚應用的許可權範圍
- 可以將數據在不同應用之間遷移
- 開發者
- 無需單獨實現賬戶註冊與用戶管理等服務
- 不需要處理複雜的加密解密等校驗邏輯
- Blockstack
- 一套 DID 身份與用戶數據管理標準
- 提供更多的應用基礎設施服務
應用授權的流程
如下所示:
- 構建 Token 並跳轉 通過 BlockStack.js 所提供的
redirectToSignIn方法跳轉到 BlockStackBrowser 完成授權- 構建請求體
authRequestgenerateAndStoreTransitKey生成一個臨時並隨機的公私鑰 Key- 返回一個經過編碼的
authRequest字元串
-
launchCustomProtocol封裝一系列的邏輯並跳轉至 BlockStackBrowser- 添加一些超時和請求序號等操作
- 構建請求體
- Browser 接收參數並解析 BlockStack 瀏覽器端接收到
authRequest參數觸發驗證流程-
app/js/auth/index.js中使用verifyAuthRequestAndLoadManifest校驗authRequest合法性並獲取 DApp 的 Manifest.json 文件verifyAuthRequest校驗 Token 的合法性fetchAppManifest獲取應用 Manifest.json 文件
-
getFreshIdentities通過用戶快取在瀏覽器中的登錄資訊addresses(地址)獲取用戶的資訊- 請求
https://core.blockstack.org/addresses/bitcoin/${address}獲得用戶比特幣地址的資訊 - 載入用戶域名資訊
- 從 Gaia 獲取用戶 profile 文件的位置,並拿到用戶的 profile 文件
- 請求
- 用戶根據 profile 中包含的身份資訊讓用戶選擇需要授權的用戶名,觸發
login- 客戶端
noCoreStorage作為監聽標誌來構造authRespon - 獲取用戶的
profileUrl - 獲取 app 的
bucketUrl - 創建並更新用戶的 Profile 中 apps 的數據
- 構建 AuthResponse Token
- 生成 appPrivateKey
- 生成 gaiaAssociationToken
- 通過 BlockStack.js 的
redirectUserToApp返回應用 - redirect URI
- 客戶端
-
- APP 接受並解析 通過
AuthRrsponse參數解析獲取用戶資訊並持久化- 調用
userSession.SignInPending或userSession.handlePendingSignIn能夠觸發對AuthResponse的解析 - 通過
verifyAuthResponse進行一系列的驗證,fetchPrivate獲得授權用戶的 profile 數據 - 持久化用戶數據到瀏覽器 localstorage
- 調用
sequenceDiagram participant A as App participant B as Broswer participant G as Gaia participant C as BlockStack Core participant Bi as Bitcoin network Note over A: Make authRequest A->>+B: authRequest Token Note over B: verifyAuthRequest B->>-A: fetch Manifest.json A->>B: Manifest.json opt getFreshIdentities B->>Bi: nameLookup names Bi->>B: get names alt is has no name B->>+G: fetchProfileLocations G->>-B: profile data else is well B->>+C: nameLookupUrl C->>-B: nameInfo with zoneFile end end note over B: render account list opt user click login B->>+C: nameLookupUrl C->>-B: profile data note over B: verify APP Scope alt is name has no zonefile B->>G: fetchProfileLocations G->>B: profile url else is has zoneFile note over B: Parse zonefile url end B->>G: getAppBucketUrl G->>B: appBucketUrl note over B: signProfileForUpload B-->> G: uploadProfile note over B: AuthResponse B->>A: AuthResponse Token end note over A: getAuthRes Token A->>G: get profile G->>A: profile data note over A: store userData
程式碼解析
構建授權請求
// 觸發授權請求 redirectToSignIn( redirectURI?: string, manifestURI?: string, scopes?: Array<AuthScope | string> ): void { const transitKey = this.generateAndStoreTransitKey() // 生成一個臨時秘鑰對 const authRequest = this.makeAuthRequest(transitKey, redirectURI, manifestURI, scopes) // 構建 AuthRequest const authenticatorURL = this.appConfig && this.appConfig.authenticatorURL // 獲取授權跳轉鏈接 return authApp.redirectToSignInWithAuthRequest(authRequest, authenticatorURL) // 跳轉 } // 構建 AuthRequest 數據 makeAuthRequest( transitKey?: string, redirectURI?: string, manifestURI?: string, scopes?: Array<AuthScope | string>, ): string { const appConfig = this.appConfig transitKey = transitKey || this.generateAndStoreTransitKey() redirectURI = redirectURI || appConfig.redirectURI() manifestURI = manifestURI || appConfig.manifestURI() scopes = scopes || appConfig.scopes return authMessages.makeAuthRequest( transitKey, redirectURI, manifestURI, scopes) } // 構建請求授權 Token 詳情 export function makeAuthRequest( transitPrivateKey?: string, redirectURI?: string, manifestURI?: string, scopes: Array<AuthScope | string> = DEFAULT_SCOPE.slice(), appDomain?: string, expiresAt: number = nextMonth().getTime(), extraParams: any = {} ): string { // ... const payload = Object.assign({}, extraParams, { jti: makeUUID4(), iat: Math.floor(new Date().getTime() / 1000), // JWT times are in seconds exp: Math.floor(expiresAt / 1000), // JWT times are in seconds iss: null, public_keys: [], domain_name: appDomain, manifest_uri: manifestURI, redirect_uri: redirectURI, version: VERSION, do_not_include_profile: true, supports_hub_url: true, scopes }) /* Convert the private key to a public key to an issuer */ const publicKey = SECP256K1Client.derivePublicKey(transitPrivateKey) payload.public_keys = [publicKey] const address = publicKeyToAddress(publicKey) payload.iss = makeDIDFromAddress(address) /* Sign and return the token */ const tokenSigner = new TokenSigner('ES256k', transitPrivateKey) const token = tokenSigner.sign(payload) // jsontokens 用私鑰簽名 return token }最終的 Token 會成為我們看到的形式
https://browser.blockstack.org/auth?authRequest=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJqdGkiOiIyYTA0Y2Q4YS1lZTBmLTQ1ZTYtYjE4MS1mNWE4YzdjMmY3NzUiLCJpYXQiOjE1ODQxNTk3MzgsImV4cCI6MTU4NDE2MzMzOCwiaXNzIjoiZGlkOmJ0Yy1hZGRyOjFCWlNXWGVUWTNoYkd3WFZBOEt2NjNoZFZGWDI5Z2JabmciLCJwdWJsaWNfa2V5cyI6WyIwMjNlMjM3MDk1NDBhNmVkOWEyNWQ0YWUzOGQ1MTcxYTVlNjljNGY4ZDhjODQzOWZjMzg2YTllYmQ3NGJmMDgyOTEiXSwiZG9tYWluX25hbWUiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJtYW5pZmVzdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvbWFuaWZlc3QuanNvbiIsInJlZGlyZWN0X3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsInZlcnNpb24iOiIxLjMuMSIsImRvX25vdF9pbmNsdWRlX3Byb2ZpbGUiOnRydWUsInN1cHBvcnRzX2h1Yl91cmwiOnRydWUsInNjb3BlcyI6WyJzdG9yZV93cml0ZSIsInB1Ymxpc2hfZGF0YSJdfQ.HFaU9K2QV_y13h-ZMqEnzvsnC2RvphfdaA3r0qaCyfBZSA0mGghDvxU0z1Mq7_HfLgq9ClVNYHJvSR9_OHZc3g
Browser 端的參數解析與數據載入
BlockStack 瀏覽器端處理 query 中的 authRequest 參數
// app/js/auth/index.js 在組建內觸發參數解析 componentWillMount() { const queryDict = queryString.parse(location.search) const echoRequestId = queryDict.echo const authRequest = getAuthRequestFromURL() // 獲取 query 中的參數 const decodedToken = decodeToken(authRequest) const { scopes } = decodedToken.payload // gaia 授權範圍 this.setState({ authRequest, echoRequestId, decodedToken, scopes: { ...this.state.scopes, email: scopes.includes('email'), publishData: scopes.includes('publish_data') } }) this.props.verifyAuthRequestAndLoadManifest(authRequest) // 校驗 authRequest 並獲取 APP 的 Manifest.json 文件 this.getFreshIdentities() // 載入用戶賬戶資訊 } getFreshIdentities = async () => { await this.props.refreshIdentities(this.props.api, this.props.addresses) this.setState({ refreshingIdentities: false }) } // 載入用戶資訊 function refreshIdentities( api, ownerAddresses ) { return async (dispatch) => { logger.info('refreshIdentities') // account.identityAccount.addresses const promises = ownerAddresses.map((address, index) => { // 根據用戶的儲存在瀏覽器本地的地址數據循環拉取用戶資訊 const promise = new Promise(resolve => { const url = api.bitcoinAddressLookupUrl.replace('{address}', address) // 比特幣網路中的地址查詢鏈接 return fetch(url) .then(response => response.text()) .then(responseText => JSON.parse(responseText)) .then(responseJson => { if (responseJson.names.length === 0) { // 沒有用戶名 const gaiaBucketAddress = ownerAddresses[0] // 默認第一個地址是用戶的 gaia 地址 return fetchProfileLocations( // 尋找 profile 的儲存位置 api.gaiaHubConfig.url_prefix, address, gaiaBucketAddress, index ).then(returnObject => { if (returnObject && returnObject.profile) { const profile = returnObject.profile const zoneFile = '' dispatch(updateProfile(index, profile, zoneFile)) // 更新現有的用戶 profile 資訊 let verifications = [] let trustLevel = 0 // ... } else { resolve() return Promise.resolve() } }) } else { const nameOwned = responseJson.names[0] dispatch(usernameOwned(index, nameOwned)) // 更新 redux const lookupUrl = api.nameLookupUrl.replace('{name}', nameOwned) // 通過用戶名查詢數據 logger.debug(`refreshIdentities: fetching: ${lookupUrl}`) return fetch(lookupUrl) .then(response => response.text()) .then(responseText => JSON.parse(responseText)) .then(lookupResponseJson => { const zoneFile = lookupResponseJson.zonefile // 獲的用戶的 zonefile const ownerAddress = lookupResponseJson.address const expireBlock = lookupResponseJson.expire_block || -1 resolve() return Promise.resolve() }) .catch(error => { dispatch(updateProfile(index, DEFAULT_PROFILE, zoneFile)) resolve() return Promise.resolve() }) }) .catch(error => { resolve() return Promise.resolve() }) } }) .catch(error => { resolve() return Promise.resolve() }) }) return promise }) return Promise.all(promises) } }localstorage 中保存了 Redux 的數據結構
Browser 端的解析和 Manifest 拉取

image
BlockStack.js // 校驗 authRequest export async function verifyAuthRequestAndLoadManifest(token: string): Promise<any> { const valid = await verifyAuthRequest(token) if (!valid) { throw new Error('Token is an invalid auth request') } return fetchAppManifest(token) } // 校驗 authRequest 的 token export async function verifyAuthRequest(token: string): Promise<boolean> { if (decodeToken(token).header.alg === 'none') { throw new Error('Token must be signed in order to be verified') } const values = await Promise.all([ isExpirationDateValid(token), isIssuanceDateValid(token), doSignaturesMatchPublicKeys(token), doPublicKeysMatchIssuer(token), isManifestUriValid(token), isRedirectUriValid(token) ]) return values.every(val => val) } // 獲取 APP 的 Manifest 文件 export async function fetchAppManifest(authRequest: string): Promise<any> { if (!authRequest) { throw new Error('Invalid auth request') } const payload = decodeToken(authRequest).payload if (typeof payload === 'string') { throw new Error('Unexpected token payload type of string') } const manifestURI = payload.manifest_uri as string try { Logger.debug(`Fetching manifest from ${manifestURI}`) const response = await fetchPrivate(manifestURI) const responseText = await response.text() const responseJSON = JSON.parse(responseText) return { ...responseJSON, manifestURI } } catch (error) { console.log(error) throw new Error('Could not fetch manifest.json') } }用戶點擊登錄之後的授權流程
// login 函數 用戶點擊多個授權之後的回調 login = (identityIndex = this.state.currentIdentityIndex) => { this.setState({ processing: true, invalidScopes: false }) // ... // if profile has no name, lookupUrl will be // http://localhost:6270/v1/names/ which returns 401 const lookupUrl = this.props.api.nameLookupUrl.replace( '{name}', lookupValue ) fetch(lookupUrl) .then(response => response.text()) .then(responseText => JSON.parse(responseText)) .then(responseJSON => { if (hasUsername) { if (responseJSON.hasOwnProperty('address')) { const nameOwningAddress = responseJSON.address if (nameOwningAddress === identity.ownerAddress) { logger.debug('login: name has propagated on the network.') this.setState({ blockchainId: lookupValue }) } else { logger.debug('login: name is not usable on the network.') hasUsername = false } } else { logger.debug('login: name is not visible on the network.') hasUsername = false } } const appDomain = this.state.decodedToken.payload.domain_name const scopes = this.state.decodedToken.payload.scopes const needsCoreStorage = !appRequestSupportsDirectHub( this.state.decodedToken.payload ) const scopesJSONString = JSON.stringify(scopes) //... // APP 校驗許可權 if (requestingStoreWrite && !needsCoreStorage) { this.setState({ noCoreStorage: true // 更新跳轉狀態 }) this.props.noCoreSessionToken(appDomain) } else { this.setState({ noCoreStorage: true // 更新跳轉狀態 }) this.props.noCoreSessionToken(appDomain) } }) } // 跳轉狀態監聽 componentWillReceiveProps(nextProps) { if (!this.state.responseSent) { // ... const appDomain = this.state.decodedToken.payload.domain_name const localIdentities = nextProps.localIdentities const identityKeypairs = nextProps.identityKeypairs if (!appDomain || !nextProps.coreSessionTokens[appDomain]) { if (this.state.noCoreStorage) { // 跳轉判斷標誌 logger.debug( 'componentWillReceiveProps: no core session token expected' ) } else { logger.debug( 'componentWillReceiveProps: no app domain or no core session token' ) return } } // ... const identityIndex = this.state.currentIdentityIndex const hasUsername = this.state.hasUsername if (hasUsername) { logger.debug(`login(): id index ${identityIndex} has no username`) } // Get keypair corresponding to the current user identity 獲得秘鑰對 const profileSigningKeypair = identityKeypairs[identityIndex] const identity = localIdentities[identityIndex] let blockchainId = null if (decodedCoreSessionToken) { blockchainId = decodedCoreSessionToken.payload.blockchain_id } else { blockchainId = this.state.blockchainId } // 獲得用戶的私鑰和 appsNodeKey const profile = identity.profile const privateKey = profileSigningKeypair.key const appsNodeKey = profileSigningKeypair.appsNodeKey const salt = profileSigningKeypair.salt let profileUrlPromise if (identity.zoneFile && identity.zoneFile.length > 0) { const zoneFileJson = parseZoneFile(identity.zoneFile) const profileUrlFromZonefile = getTokenFileUrlFromZoneFile(zoneFileJson) // 用 zonefile 獲取用戶資訊 if ( profileUrlFromZonefile !== null && profileUrlFromZonefile !== undefined ) { profileUrlPromise = Promise.resolve(profileUrlFromZonefile) } } const gaiaBucketAddress = nextProps.identityKeypairs[0].address const identityAddress = nextProps.identityKeypairs[identityIndex].address const gaiaUrlBase = nextProps.api.gaiaHubConfig.url_prefix // 沒有 profile 就從 gaia 中查詢 if (!profileUrlPromise) { // use default Gaia hub if we can't tell from the profile where the profile Gaia hub is profileUrlPromise = fetchProfileLocations( gaiaUrlBase, identityAddress, gaiaBucketAddress, identityIndex ).then(fetchProfileResp => { if (fetchProfileResp && fetchProfileResp.profileUrl) { return fetchProfileResp.profileUrl } else { return getDefaultProfileUrl(gaiaUrlBase, identityAddress) } }) } profileUrlPromise.then(async profileUrl => { const appPrivateKey = await BlockstackWallet.getLegacyAppPrivateKey(appsNodeKey, salt, appDomain) // 獲得 APP 的 PrivateKey // Add app storage bucket URL to profile if publish_data scope is requested if (this.state.scopes.publishData) { let apps = {} if (profile.hasOwnProperty('apps')) { apps = profile.apps // 獲得用戶的 apps 配置 } if (storageConnected) { const hubUrl = this.props.api.gaiaHubUrl await getAppBucketUrl(hubUrl, appPrivateKey) // 根據用戶的授權歷史在 apps 查找授權 APP 的 bucket 位置,沒有則創建新的 .then(appBucketUrl => { logger.debug( `componentWillReceiveProps: appBucketUrl ${appBucketUrl}` ) apps[appDomain] = appBucketUrl // bucketurl logger.debug( `componentWillReceiveProps: new apps array ${JSON.stringify( apps )}` ) profile.apps = apps const signedProfileTokenData = signProfileForUpload( // 更新用戶的 profile profile, nextProps.identityKeypairs[identityIndex], this.props.api ) logger.debug( 'componentWillReceiveProps: uploading updated profile with new apps array' ) return uploadProfile( this.props.api, identity, nextProps.identityKeypairs[identityIndex], signedProfileTokenData ) }) .then(() => this.completeAuthResponse( // 構建 AuthResponse privateKey, blockchainId, coreSessionToken, appPrivateKey, profile, profileUrl ) ) } // ... } else { await this.completeAuthResponse( privateKey, blockchainId, coreSessionToken, appPrivateKey, profile, profileUrl ) } }) } } // 構造授權之後的返回 const authResponse = await makeAuthResponse( privateKey, profileResponseData, blockchainId, metadata, coreSessionToken, appPrivateKey, undefined, transitPublicKey, hubUrl, blockstackAPIUrl, associationToken ) redirectUserToApp(this.state.authRequest, authResponse) // 重定向回 APP 頁面 const payload = decodeToken(authRequest).payload if (typeof payload === 'string') { throw new Error('Unexpected token payload type of string') } let redirectURI = payload.redirect_uri as string Logger.debug(redirectURI) if (redirectURI) { redirectURI = updateQueryStringParameter(redirectURI, 'authResponse', authResponse) } else { throw new Error('Invalid redirect URI') } const location = getGlobalObject('location', { throwIfUnavailable: true, usageDesc: 'redirectUserToApp' }) kk = redirectURI }最後我們得到
http://localhost:3000/?authResponse=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJqdGkiOiIyMzA3NmE1NC0zYmVkLTRiYjEtOGZlOC0yY2I1MDgyNjBiOGIiLCJpYXQiOjE1ODQyNTU1MzksImV4cCI6MTU4NjkzMzg2NSwiaXNzIjoiZGlkOmJ0Yy1hZGRyOjE4d0xaanozRWFUdzd5d2o0a3hlZjR1eWlMWDFmeW9GWDMiLCJwcml2YXRlX2tleSI6IjdiMjI2OTc2MjIzYTIyNjQzNDY0NjQ2NjY0Mzg2NjM4MzAzMTM0NjQzMjYxMzY2NjM4MzAzOTM1NjEzNTY1MzIzMjMxMzY2NDYxNjM2MzIyMmMyMjY1NzA2ODY1NmQ2NTcyNjE2YzUwNGIyMjNhMjIzMDMyNjI2NDYzMzUzNjMwNjIzODY0MzY2NTM2NjEzNjY1MzEzNjM2NjEzNzM0NjQzMzM5MzYzNjM2NjUzNzYyMzMzODYxMzkzMjY1Mzg2MzMxNjIzMDM2MzY2MzY1MzAzOTY1MzUzMTM2NjIzMjYyNjE2NDYzMzIzMjM1NjMzNjMxMzMyMjJjMjI2MzY5NzA2ODY1NzI1NDY1Nzg3NDIyM2EyMjM5NjIzMzYyMzg2NDM5MzYzMjM4NjQzNjM0MzU2MTMxMzYzMzM0MzQzNTY1NjM2NTY0NjEzMzM4NjUzOTYxMzY2NjY1MzQzMTMyMzYzMjM5NjQ2MjY0NjE2NjY2NjMzNDMzMzAzNTM5NjIzNjYzNjUzNjM5NjMzMTYyMzczOTYyMzkzMzYyNjEzNTM4MzkzNjY2NjUzNzM5NjY2MTMyMzYzNjM5NjQ2NjM3MzkzMzM5MzUzNTMyNjQzNDYxMzYzNjM0Mzc2MzM5MzkzNDYyMzEzODM5MzE2NDMxMzEzNTYzMzE2NTM3MzkzNzY1MzMzMDY1MzI2NDM1MzM2NDYxMzEzODM5MzA2MTM1NjUzODMzMzc2NDM0MzUzNzY0MzIzNzM5MzI2MTMyMzMzMDY2MzgzNjYzMzkzOTMyNjQ2MjYxMjIyYzIyNmQ2MTYzMjIzYTIyNjI2NTY0MzMzMDY2MzQ2MzMyMzI2MTY1MzA2MzY0MzUzNTM1Mzc2NTYxMzQ2MzMxMzk2MjYxNjQzMzM3MzU2MjMxMzQzODM4NjEzMDM4NjQzMzY0NjIzOTMyMzQ2NTMwNjYzOTMzMzgzNjM0MzMzMDM0MzEzNTMxMzUzODM1NjQyMjJjMjI3NzYxNzM1Mzc0NzI2OTZlNjcyMjNhNzQ3Mjc1NjU3ZCIsInB1YmxpY19rZXlzIjpbIjAzOWM4OTg3OWZmNTZhMzVkOWU0MjYzYTI5ZjI5YmQyZTZjMzE1Y2UyMTZiMzYyMDZkZDA3NDg5OWMxMWJhNmRjYSJdLCJwcm9maWxlIjpudWxsLCJ1c2VybmFtZSI6Il9fY2Fvc19fLmlkLmJsb2Nrc3RhY2siLCJjb3JlX3Rva2VuIjpudWxsLCJlbWFpbCI6bnVsbCwicHJvZmlsZV91cmwiOiJodHRwczovL2dhaWEuYmxvY2tzdGFjay5vcmcvaHViLzE4d0xaanozRWFUdzd5d2o0a3hlZjR1eWlMWDFmeW9GWDMvcHJvZmlsZS5qc29uIiwiaHViVXJsIjoiaHR0cHM6Ly9odWIuYmxvY2tzdGFjay5vcmciLCJibG9ja3N0YWNrQVBJVXJsIjoiaHR0cHM6Ly9jb3JlLmJsb2Nrc3RhY2sub3JnIiwiYXNzb2NpYXRpb25Ub2tlbiI6ImV5SjBlWEFpT2lKS1YxUWlMQ0poYkdjaU9pSkZVekkxTmtzaWZRLmV5SmphR2xzWkZSdlFYTnpiMk5wWVhSbElqb2lNREprWkRKbFlXTTFaamcxTURFMllqRXlNV0kwWlRBME16SmxOREkyTkRabFlXTXhOVFkyWTJZeFpqVTVZemN4T1dZeE5UWmxPR0UyTm1ZNE1XSTRZek5pSWl3aWFYTnpJam9pTURNNVl6ZzVPRGM1Wm1ZMU5tRXpOV1E1WlRReU5qTmhNamxtTWpsaVpESmxObU16TVRWalpUSXhObUl6TmpJd05tUmtNRGMwT0RrNVl6RXhZbUUyWkdOaElpd2laWGh3SWpveE5qRTFOemt4TkRRMkxqRTROeXdpYVdGMElqb3hOVGcwTWpVMU5EUTJMakU0Tnl3aWMyRnNkQ0k2SWpjMk1UYzNZV1U1T0RWa01EVmtOell5TmpVMFltTmtZVEJsTkRReE16Y3hJbjAuMThxOTZJeW5DWTJFclRMdGtsOG05dUpQR2tickRLaXgxY3Y2NDFhOC1RRnZHT1BIODM1S0FrZm1rd19nNVRZM2lSamQxcGt3VG44Y2F1ZFhLQWY2TVEiLCJ2ZXJzaW9uIjoiMS4zLjEifQ.KGRfgjzPkQ3Y66ek2EjS2XT8EFeRc9FoElnxrsPJOCN3_YBibRpvaYPVbUkMXAqqVM6jIzlJBfdvFI3jN4O5Cg
App 端的解析與處理
app 端的數據通過 userSession.SignInPending 或 userSession.handlePendingSignIn 解析 authResponse 參數
export async function handlePendingSignIn( nameLookupURL: string = '', authResponseToken: string = getAuthResponseToken(), transitKey?: string, caller?: UserSession ): Promise<UserData> { if (!caller) { caller = new UserSession() } const sessionData = caller.store.getSessionData() if (!transitKey) { transitKey = caller.store.getSessionData().transitKey } if (!nameLookupURL) { let coreNode = caller.appConfig && caller.appConfig.coreNode if (!coreNode) { coreNode = config.network.blockstackAPIUrl } const tokenPayload = decodeToken(authResponseToken).payload if (typeof tokenPayload === 'string') { throw new Error('Unexpected token payload type of string') } if (isLaterVersion(tokenPayload.version as string, '1.3.0') && tokenPayload.blockstackAPIUrl !== null && tokenPayload.blockstackAPIUrl !== undefined) { config.network.blockstackAPIUrl = tokenPayload.blockstackAPIUrl as string coreNode = tokenPayload.blockstackAPIUrl as string } nameLookupURL = `${coreNode}${NAME_LOOKUP_PATH}` } const isValid = await verifyAuthResponse(authResponseToken, nameLookupURL) // 校驗 authResponse Token if (!isValid) { throw new LoginFailedError('Invalid authentication response.') } const tokenPayload = decodeToken(authResponseToken).payload // TODO: real version handling let appPrivateKey = tokenPayload.private_key as string let coreSessionToken = tokenPayload.core_token as string // ... let hubUrl = BLOCKSTACK_DEFAULT_GAIA_HUB_URL let gaiaAssociationToken: string const userData: UserData = { username: tokenPayload.username as string, profile: tokenPayload.profile, email: tokenPayload.email as string, decentralizedID: tokenPayload.iss, identityAddress: getAddressFromDID(tokenPayload.iss), appPrivateKey, coreSessionToken, authResponseToken, hubUrl, coreNode: tokenPayload.blockstackAPIUrl as string, gaiaAssociationToken } const profileURL = tokenPayload.profile_url as string if (!userData.profile && profileURL) { const response = await fetchPrivate(profileURL) // 拉取用戶 profile 資訊 if (!response.ok) { // return blank profile if we fail to fetch userData.profile = Object.assign({}, DEFAULT_PROFILE) } else { const responseText = await response.text() const wrappedProfile = JSON.parse(responseText) const profile = extractProfile(wrappedProfile[0].token) userData.profile = profile } } else { userData.profile = tokenPayload.profile } sessionData.userData = userData caller.store.setSessionData(sessionData) // 快取用戶數據到 return userData // 返回結果 }userData 最後的樣子(Redux)

image
- name - 用戶的域名
- profile - 域名下的身份資訊
- email - 用戶的郵箱資訊
- decentralizedID - DID
- identityAddress - 用戶身份的 BTC 地址
- appPrivateKey - app 應用的私鑰
- coreSessionToken - V2 預留
- authResponseToken - browser 返回的授權資訊 Token
- hubUrl - gaia hub 的地址
- gaiaAssociationToken - app 與 gaia 交互所需要的 token
- gaiaHubConfig - gaia 伺服器的配置資訊
KeyPairs && appPrivateKey
const appPrivateKey = await BlockstackWallet.getLegacyAppPrivateKey(appsNodeKey, salt, appDomain) // src/[email protected]// 獲取身份的密鑰對 getIdentityKeyPair(addressIndex: number, alwaysUncompressed: boolean = false): IdentityKeyPair { const identityNode = this.getIdentityAddressNode(addressIndex) const address = BlockstackWallet.getAddressFromBIP32Node(identityNode) let identityKey = getNodePrivateKey(identityNode) if (alwaysUncompressed && identityKey.length === 66) { identityKey = identityKey.slice(0, 64) } const identityKeyID = getNodePublicKey(identityNode) const appsNodeKey = BlockstackWallet.getAppsNode(identityNode).toBase58() // 獲取 appsNodeKey const salt = this.getIdentitySalt() const keyPair = { key: identityKey, keyID: identityKeyID, address, appsNodeKey, salt } return keyPair } } // 獲取 appPrivateKey static getLegacyAppPrivateKey(appsNodeKey: string, salt: string, appDomain: string): string { const appNode = getLegacyAppNode(appsNodeKey, salt, appDomain) return getNodePrivateKey(appNode).slice(0, 64) } function getNodePrivateKey(node: BIP32Interface): string { return ecPairToHexString(ECPair.fromPrivateKey(node.privateKey)) } // src/storage/[email protected] // 使用 appPrivateKey 加密內容 export async function encryptContent( content: string | Buffer, options?: EncryptContentOptions, caller?: UserSession ): Promise<string> { const opts = Object.assign({}, options) let privateKey: string if (!opts.publicKey) { privateKey = (caller || new UserSession()).loadUserData().appPrivateKey opts.publicKey = getPublicKeyFromPrivate(privateKey) } // ... const contentBuffer = typeof content === 'string' ? Buffer.from(content) : content const cipherObject = await encryptECIES(opts.publicKey, contentBuffer, wasString, opts.cipherTextEncoding) let cipherPayload = JSON.stringify(cipherObject) // ... return cipherPayload } // 使用 appPrivateKey 解密內容 export function decryptContent( content: string, options?: { privateKey?: string }, caller?: UserSession ): Promise<string | Buffer> { const opts = Object.assign({}, options) if (!opts.privateKey) { opts.privateKey = (caller || new UserSession()).loadUserData().appPrivateKey } try { const cipherObject = JSON.parse(content) return decryptECIES(opts.privateKey, cipherObject) } catch (err) { }
持久化 Redux 數據
// app/js/store/configure.js@browser import persistState from 'redux-localstorage' // 持久化 Redux 中間件 export default function configureStore(initialState) { return finalCreateStore(RootReducer, initialState) } const finalCreateStore = composeEnhancers( applyMiddleware(thunk), persistState(null, { // persistState 持久化 // eslint-disable-next-line slicer: paths => state => ({ ...state, auth: AuthInitialState, notifications: [] }) }) )(createStore)userData 也會保存在 localstorage 中

image
localstorage 的保存
export class UserSession { // ... constructor(options?: { appConfig?: AppConfig, sessionStore?: SessionDataStore, sessionOptions?: SessionOptions }) { // ... if (options && options.sessionStore) { this.store = options.sessionStore } else if (runningInBrowser) { if (options) { this.store = new LocalStorageStore(options.sessionOptions) } else { this.store = new LocalStorageStore() } } else if (options) { this.store = new InstanceDataStore(options.sessionOptions) } else { this.store = new InstanceDataStore() } } } // 繼承並覆蓋 setSessionData ,持久化數據到 LocalStorage export class LocalStorageStore extends SessionDataStore { key: string constructor(sessionOptions?: SessionOptions) { super(sessionOptions) //... setSessionData(session: SessionData): boolean { localStorage.setItem(this.key, session.toString()) return true } }2020-03-15 由助教曹帥整理

