专栏首页华仔的技术笔记BlockStack身份授权流程

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.SignInPendinguserSession.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.SignInPendinguserSession.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/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 数据

// 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 由助教曹帅整理

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 生产环境小程序登录出现502

    用的是腾讯wafer的解决方案: 生产环境部署说明 https://cloud.tencent.com/document/product/619/11689

    rectinajh
  • Boinc for Linux,在服务器上一边科研,一边挖矿

    https://www.toutiao.com/i6589514965932048910/

    rectinajh
  • iOS无线安装企业账号应用

    rectinajh
  • 智享产学前沿 ,慧及青年学者 | 2017年度CCF-腾讯犀牛鸟基金获奖结果揭晓

    近日,2017 CCF-腾讯犀牛鸟基金评审环节落下帷幕。经过基金特邀专家对项目深入细致的函评及现场决议,综合考量申请者的课题前瞻性、思维创新性、应用可行性及团队...

    腾讯高校合作
  • 2017年CCF-腾讯犀牛鸟基金终期评审会暨颁奖典礼在CNCC期间举办

    CCF-腾讯犀牛鸟基金由中国计算机学会(简称CCF)和腾讯公司联合发起,旨在助力全球青年学者把握时代赋予的机遇,开展致力于提升人类生活品质的创新研究,推动科研成...

    腾讯高校合作
  • Android自定义控件之ToggleImageView

    张风捷特烈
  • 2020年前端学习的新路径

    不过本文更多的还是想给大家带来下一个时代前端开发学习和进阶的思考,而不是纯标题党。就像今年前端领域发生了很多事情,却没有了前几年的热闹非凡的感觉:

    前端迷
  • 反思录:Angular实现svg和png图片下载

    我经常思考,在面临一个不确定问题时,以往的经验究竟有无辅助作用?如果把经验遗忘会产生何种程度的影响?在上下求索未果之后,如何找回曾经的感觉,恰若灵光一现?凡此种...

    lambeta
  • Zipack初体验:我的开源标准!

    当今最流行的序列化格式无疑是JSON,但是基于文本的JSON有许多缺点,比如解析速度慢,体积较大。根本原因在于,JSON是基于文本的,只要是文本就离不开编译,只...

    Jean
  • Jquery使用jsonp跨域访问

    forrestlin

扫码关注云+社区

领取腾讯云代金券