BlockStack身份授权流程

2020-04-01 19:07:22 浏览数 (1)

为什么需要应用授权

去中心化身份的前提条件,是在同一个身份平台所能覆盖的范围内,用户的身份识别和检测标准统一,作为区块链应用开发基础设施的服务提供商,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.SignInPendinguserSession.handlePendingSignIn 能够触发对 AuthResponse 的解析
    • 通过verifyAuthResponse 进行一系列的验证,fetchPrivate 获得授权用户的 profile 数据
    • 持久化用户数据到浏览器 localstorage
代码语言:javascript复制
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

代码解析

构建授权请求

代码语言:javascript复制
// 触发授权请求
 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 会成为我们看到的形式

代码语言:javascript复制
https://browser.blockstack.org/auth?authRequest=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJqdGkiOiIyYTA0Y2Q4YS1lZTBmLTQ1ZTYtYjE4MS1mNWE4YzdjMmY3NzUiLCJpYXQiOjE1ODQxNTk3MzgsImV4cCI6MTU4NDE2MzMzOCwiaXNzIjoiZGlkOmJ0Yy1hZGRyOjFCWlNXWGVUWTNoYkd3WFZBOEt2NjNoZFZGWDI5Z2JabmciLCJwdWJsaWNfa2V5cyI6WyIwMjNlMjM3MDk1NDBhNmVkOWEyNWQ0YWUzOGQ1MTcxYTVlNjljNGY4ZDhjODQzOWZjMzg2YTllYmQ3NGJmMDgyOTEiXSwiZG9tYWluX25hbWUiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJtYW5pZmVzdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvbWFuaWZlc3QuanNvbiIsInJlZGlyZWN0X3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsInZlcnNpb24iOiIxLjMuMSIsImRvX25vdF9pbmNsdWRlX3Byb2ZpbGUiOnRydWUsInN1cHBvcnRzX2h1Yl91cmwiOnRydWUsInNjb3BlcyI6WyJzdG9yZV93cml0ZSIsInB1Ymxpc2hfZGF0YSJdfQ.HFaU9K2QV_y13h-ZMqEnzvsnC2RvphfdaA3r0qaCyfBZSA0mGghDvxU0z1Mq7_HfLgq9ClVNYHJvSR9_OHZc3g

Browser 端的参数解析与数据加载

BlockStack 浏览器端处理 query 中的 authRequest 参数

代码语言:javascript复制
// 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

代码语言:javascript复制
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')
  }
}

用户点击登录之后的授权流程

代码语言:javascript复制
// 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
}

最后我们得到

代码语言:javascript复制
http://localhost:3000/?authResponse=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJqdGkiOiIyMzA3NmE1NC0zYmVkLTRiYjEtOGZlOC0yY2I1MDgyNjBiOGIiLCJpYXQiOjE1ODQyNTU1MzksImV4cCI6MTU4NjkzMzg2NSwiaXNzIjoiZGlkOmJ0Yy1hZGRyOjE4d0xaanozRWFUdzd5d2o0a3hlZjR1eWlMWDFmeW9GWDMiLCJwcml2YXRlX2tleSI6IjdiMjI2OTc2MjIzYTIyNjQzNDY0NjQ2NjY0Mzg2NjM4MzAzMTM0NjQzMjYxMzY2NjM4MzAzOTM1NjEzNTY1MzIzMjMxMzY2NDYxNjM2MzIyMmMyMjY1NzA2ODY1NmQ2NTcyNjE2YzUwNGIyMjNhMjIzMDMyNjI2NDYzMzUzNjMwNjIzODY0MzY2NTM2NjEzNjY1MzEzNjM2NjEzNzM0NjQzMzM5MzYzNjM2NjUzNzYyMzMzODYxMzkzMjY1Mzg2MzMxNjIzMDM2MzY2MzY1MzAzOTY1MzUzMTM2NjIzMjYyNjE2NDYzMzIzMjM1NjMzNjMxMzMyMjJjMjI2MzY5NzA2ODY1NzI1NDY1Nzg3NDIyM2EyMjM5NjIzMzYyMzg2NDM5MzYzMjM4NjQzNjM0MzU2MTMxMzYzMzM0MzQzNTY1NjM2NTY0NjEzMzM4NjUzOTYxMzY2NjY1MzQzMTMyMzYzMjM5NjQ2MjY0NjE2NjY2NjMzNDMzMzAzNTM5NjIzNjYzNjUzNjM5NjMzMTYyMzczOTYyMzkzMzYyNjEzNTM4MzkzNjY2NjUzNzM5NjY2MTMyMzYzNjM5NjQ2NjM3MzkzMzM5MzUzNTMyNjQzNDYxMzYzNjM0Mzc2MzM5MzkzNDYyMzEzODM5MzE2NDMxMzEzNTYzMzE2NTM3MzkzNzY1MzMzMDY1MzI2NDM1MzM2NDYxMzEzODM5MzA2MTM1NjUzODMzMzc2NDM0MzUzNzY0MzIzNzM5MzI2MTMyMzMzMDY2MzgzNjYzMzkzOTMyNjQ2MjYxMjIyYzIyNmQ2MTYzMjIzYTIyNjI2NTY0MzMzMDY2MzQ2MzMyMzI2MTY1MzA2MzY0MzUzNTM1Mzc2NTYxMzQ2MzMxMzk2MjYxNjQzMzM3MzU2MjMxMzQzODM4NjEzMDM4NjQzMzY0NjIzOTMyMzQ2NTMwNjYzOTMzMzgzNjM0MzMzMDM0MzEzNTMxMzUzODM1NjQyMjJjMjI3NzYxNzM1Mzc0NzI2OTZlNjcyMjNhNzQ3Mjc1NjU3ZCIsInB1YmxpY19rZXlzIjpbIjAzOWM4OTg3OWZmNTZhMzVkOWU0MjYzYTI5ZjI5YmQyZTZjMzE1Y2UyMTZiMzYyMDZkZDA3NDg5OWMxMWJhNmRjYSJdLCJwcm9maWxlIjpudWxsLCJ1c2VybmFtZSI6Il9fY2Fvc19fLmlkLmJsb2Nrc3RhY2siLCJjb3JlX3Rva2VuIjpudWxsLCJlbWFpbCI6bnVsbCwicHJvZmlsZV91cmwiOiJodHRwczovL2dhaWEuYmxvY2tzdGFjay5vcmcvaHViLzE4d0xaanozRWFUdzd5d2o0a3hlZjR1eWlMWDFmeW9GWDMvcHJvZmlsZS5qc29uIiwiaHViVXJsIjoiaHR0cHM6Ly9odWIuYmxvY2tzdGFjay5vcmciLCJibG9ja3N0YWNrQVBJVXJsIjoiaHR0cHM6Ly9jb3JlLmJsb2Nrc3RhY2sub3JnIiwiYXNzb2NpYXRpb25Ub2tlbiI6ImV5SjBlWEFpT2lKS1YxUWlMQ0poYkdjaU9pSkZVekkxTmtzaWZRLmV5SmphR2xzWkZSdlFYTnpiMk5wWVhSbElqb2lNREprWkRKbFlXTTFaamcxTURFMllqRXlNV0kwWlRBME16SmxOREkyTkRabFlXTXhOVFkyWTJZeFpqVTVZemN4T1dZeE5UWmxPR0UyTm1ZNE1XSTRZek5pSWl3aWFYTnpJam9pTURNNVl6ZzVPRGM1Wm1ZMU5tRXpOV1E1WlRReU5qTmhNamxtTWpsaVpESmxObU16TVRWalpUSXhObUl6TmpJd05tUmtNRGMwT0RrNVl6RXhZbUUyWkdOaElpd2laWGh3SWpveE5qRTFOemt4TkRRMkxqRTROeXdpYVdGMElqb3hOVGcwTWpVMU5EUTJMakU0Tnl3aWMyRnNkQ0k2SWpjMk1UYzNZV1U1T0RWa01EVmtOell5TmpVMFltTmtZVEJsTkRReE16Y3hJbjAuMThxOTZJeW5DWTJFclRMdGtsOG05dUpQR2tickRLaXgxY3Y2NDFhOC1RRnZHT1BIODM1S0FrZm1rd19nNVRZM2lSamQxcGt3VG44Y2F1ZFhLQWY2TVEiLCJ2ZXJzaW9uIjoiMS4zLjEifQ.KGRfgjzPkQ3Y66ek2EjS2XT8EFeRc9FoElnxrsPJOCN3_YBibRpvaYPVbUkMXAqqVM6jIzlJBfdvFI3jN4O5Cg

App 端的解析与处理

app 端的数据通过 userSession.SignInPendinguserSession.handlePendingSignIn 解析 authResponse 参数

代码语言:javascript复制
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
代码语言:javascript复制
  const appPrivateKey = await BlockstackWallet.getLegacyAppPrivateKey(appsNodeKey, salt, appDomain)

// src/wallet.ts@blockstack.js// 获取身份的密钥对

 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/index.ts@blockstack.js
// 使用 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 数据
代码语言:javascript复制
// 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 的保存
代码语言:javascript复制
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 由助教曹帅整理

0 人点赞