BlockStack身份授權流程

為什麼需要應用授權

去中心化身份的前提條件,是在同一個身份平台所能覆蓋的範圍內,用戶的身份識別和檢測標準統一,作為區塊鏈應用開發基礎設施的服務提供商,BlockStack 在數據許可權上將應用許可權和用戶身份/數據分離,保障用戶數據所有權。

這種設計雖然實現起來較為複雜,且需要多種類型的服務提供支援,但不論是對用戶,開發者,還是整個 Blockstack 生態,都是非常優雅的方案。

mmexport1585486832221.jpg

  • 用戶
    • gaia 通過 app 域名隔離數據許可權,無需擔心全量數據安全
    • 可以使用多身份來管理相同的應用數據
    • 使用應用之前明確的清楚應用的許可權範圍
    • 可以將數據在不同應用之間遷移
  • 開發者
    • 無需單獨實現賬戶註冊與用戶管理等服務
    • 不需要處理複雜的加密解密等校驗邏輯
  • Blockstack
    • 一套 DID 身份與用戶數據管理標準
    • 提供更多的應用基礎設施服務

應用授權的流程

如下所示:

  • 構建 Token 並跳轉 通過 BlockStack.js 所提供的 redirectToSignIn 方法跳轉到 BlockStackBrowser 完成授權
    • 構建請求體 authRequest
      • generateAndStoreTransitKey 生成一個臨時並隨機的公私鑰 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 由助教曹帥整理

Tags: