Skip to content

Commit

Permalink
fix: more circular dependencies removed from auth package
Browse files Browse the repository at this point in the history
  • Loading branch information
yknl committed Aug 24, 2020
1 parent 2959c20 commit 3a1dfba
Show file tree
Hide file tree
Showing 7 changed files with 405 additions and 238 deletions.
401 changes: 198 additions & 203 deletions packages/auth/src/auth.ts

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions packages/auth/src/constants.ts
Expand Up @@ -12,6 +12,14 @@ export const BLOCKSTACK_STORAGE_LABEL = 'blockstack'
*/
export const DEFAULT_BLOCKSTACK_HOST = 'https://browser.blockstack.org/auth'

/**
* Default user profile object
*/
export const DEFAULT_PROFILE = {
'@type': 'Person',
'@context': 'http://schema.org'
}

/**
* Non-exhaustive list of common permission scopes.
*/
Expand Down
3 changes: 0 additions & 3 deletions packages/auth/src/index.ts
Expand Up @@ -13,9 +13,6 @@ export {
doPublicKeysMatchIssuer, doSignaturesMatchPublicKeys,
isManifestUriValid, isRedirectUriValid, verifyAuthRequestAndLoadManifest
} from './verification'
export {
handlePendingSignIn, signUserOut
} from './auth'
export {
UserSession
} from './userSession'
Expand Down
9 changes: 1 addition & 8 deletions packages/auth/src/messages.ts
Expand Up @@ -6,8 +6,6 @@ import { makeUUID4, nextMonth, getGlobalObject, Logger } from '@stacks/common'
import { makeDIDFromAddress } from './dids'
import { encryptECIES, decryptECIES, makeECPrivateKey, publicKeyToAddress } from '@stacks/encryption'
import { DEFAULT_SCOPE, AuthScope } from './constants'
import { UserSession } from './userSession'


const VERSION = '1.3.1'

Expand All @@ -29,7 +27,6 @@ export function generateTransitKey() {
return transitKey
}


/**
* Generates an authentication request that can be sent to the Blockstack
* browser for the user to approve sign in. This authentication request can
Expand All @@ -52,18 +49,14 @@ export function generateTransitKey() {
* @return {String} the authentication request
*/
export function makeAuthRequest(
transitPrivateKey?: string,
transitPrivateKey: string,
redirectURI?: string,
manifestURI?: string,
scopes: Array<AuthScope | string> = DEFAULT_SCOPE.slice(),
appDomain?: string,
expiresAt: number = nextMonth().getTime(),
extraParams: any = {}
): string {
if (!transitPrivateKey) {
transitPrivateKey = new UserSession().generateAndStoreTransitKey()
}

const getWindowOrigin = (paramName: string) => {
const location = getGlobalObject('location', {
throwIfUnavailable: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/auth/src/sessionData.ts
@@ -1,5 +1,5 @@
import { InvalidStateError } from '@stacks/common'
import { UserData } from './auth'
import { UserData } from './userData'

const SESSION_VERSION = '1.0.0'

Expand Down
39 changes: 39 additions & 0 deletions packages/auth/src/userData.ts
@@ -0,0 +1,39 @@
/**
* Returned from the [[UserSession.loadUserData]] function.
*/
export interface UserData {
// public: the blockstack ID (for example: stackerson.id or alice.blockstack.id)
username: string;
// public: the email address for the user. only available if the `email`
// scope is requested, and if the user has entered a valid email into
// their profile.
//
// **Note**: Blockstack does not require email validation
// for users for privacy reasons and blah blah (something like this, idk)
email?: string;
// probably public: (a quick description of what this is, and a link to the
// DID foundation and/or the blockstack docs related to DID, idk)
decentralizedID: string;
// probably private: looks like it happens to be the btc address but idk
// the value of establishing this as a supported field
identityAddress: string;
// probably public: this is an advanced feature, I think many app devs
// using our more advanced encryption functions (as opposed to putFile/getFile),
// are probably using this. seems useful to explain.
appPrivateKey: string;
// maybe public: possibly useful for advanced devs / webapps. I see an opportunity
// to make a small plug about "user owned data" here, idk.
hubUrl: string;
coreNode: string;
// maybe private: this would be an advanced field for app devs to use.
authResponseToken: string;
// private: does not get sent to webapp at all.
coreSessionToken?: string;
// private: does not get sent to webapp at all.
gaiaAssociationToken?: string;
// public: this is the proper `Person` schema json for the user.
// This is the data that gets used when the `new blockstack.Person(profile)` class is used.
profile: any;
// private: does not get sent to webapp at all.
gaiaHubConfig?: any;
}
181 changes: 158 additions & 23 deletions packages/auth/src/userSession.ts
Expand Up @@ -5,19 +5,31 @@ import {
SessionDataStore,
InstanceDataStore
} from './sessionStore'

import { decodeToken } from 'jsontokens'
import { verifyAuthResponse } from './verification'
import * as authMessages from './messages'

import { hexStringToECPair } from '@stacks/encryption'
import { getAddressFromDID } from './dids'
import {
nextHour,
MissingParameterError,
InvalidStateError,
Logger
Logger,
getGlobalObject,
LoginFailedError,
isLaterVersion,
fetchPrivate,
BLOCKSTACK_DEFAULT_GAIA_HUB_URL
} from '@stacks/common'
import { AuthScope } from './constants'
import { handlePendingSignIn, signUserOut, getAuthResponseToken } from './auth'
import { extractProfile } from '@stacks/profile'
import { AuthScope, DEFAULT_PROFILE } from './constants'
import { encryptContent, decryptContent, EncryptContentOptions } from '@stacks/encryption';

import * as queryString from 'query-string'
import { UserData } from './userData'
import { StacksMainnet } from '@stacks/network'
import {
NAME_LOOKUP_PATH
} from './constants'

/**
*
Expand Down Expand Up @@ -138,14 +150,19 @@ export class UserSession {
}

/**
* Retrieve the authentication token from the URL query.
*
* @returns {String} the authentication token if it exists otherwise `null`
* Retrieve the authentication token from the URL query
* @return {String} the authentication token if it exists otherwise `null`
*/
getAuthResponseToken(): string {
return getAuthResponseToken()
const search = getGlobalObject(
'location',
{ throwIfUnavailable: true, usageDesc: 'getAuthResponseToken' }
).search
const queryDict = queryString.parse(search)
return queryDict.authResponse ? <string>queryDict.authResponse : ''
}


/**
* Check if a user is currently signed in.
*
Expand All @@ -163,9 +180,122 @@ export class UserSession {
* @returns {Promise} that resolves to the user data object if successful and rejects
* if handling the sign in request fails or there was no pending sign in request.
*/
handlePendingSignIn(authResponseToken: string = this.getAuthResponseToken()) {
const transitKey = this.store.getSessionData().transitKey
return handlePendingSignIn(undefined, authResponseToken, transitKey, this)
async handlePendingSignIn(authResponseToken: string = this.getAuthResponseToken()): Promise<UserData> {
const sessionData = this.store.getSessionData()

if (sessionData.userData) {
throw new LoginFailedError('Existing user session found.')
}

let transitKey = this.store.getSessionData().transitKey


let nameLookupURL;
let coreNode = this.appConfig && this.appConfig.coreNode
if (!coreNode) {
let network = new StacksMainnet()
coreNode = network.coreApiUrl
}

let tokenPayload = decodeToken(authResponseToken).payload

if (typeof tokenPayload === 'string') {
throw new Error('Unexpected token payload type of string')
}

// Section below is removed since the config was never persisted and therefore useless

// if (isLaterVersion(tokenPayload.version as string, '1.3.0')
// && tokenPayload.blockstackAPIUrl !== null && tokenPayload.blockstackAPIUrl !== undefined) {
// // override globally
// Logger.info(`Overriding ${config.network.blockstackAPIUrl} `
// + `with ${tokenPayload.blockstackAPIUrl}`)
// // TODO: this config is never saved so the user node preference
// // is not respected in later sessions..
// config.network.blockstackAPIUrl = tokenPayload.blockstackAPIUrl as string
// coreNode = tokenPayload.blockstackAPIUrl as string
// }

nameLookupURL = `${coreNode}${NAME_LOOKUP_PATH}`

const isValid = await verifyAuthResponse(authResponseToken, nameLookupURL)
if (!isValid) {
throw new LoginFailedError('Invalid authentication response.')
}

// TODO: real version handling
let appPrivateKey = tokenPayload.private_key as string
let coreSessionToken = tokenPayload.core_token as string
if (isLaterVersion(tokenPayload.version as string, '1.1.0')) {
if (transitKey !== undefined && transitKey != null) {
if (tokenPayload.private_key !== undefined && tokenPayload.private_key !== null) {
try {
appPrivateKey = await authMessages.decryptPrivateKey(transitKey, tokenPayload.private_key as string)
} catch (e) {
Logger.warn('Failed decryption of appPrivateKey, will try to use as given')
try {
hexStringToECPair(tokenPayload.private_key as string)
} catch (ecPairError) {
throw new LoginFailedError('Failed decrypting appPrivateKey. Usually means'
+ ' that the transit key has changed during login.')
}
}
}
if (coreSessionToken !== undefined && coreSessionToken !== null) {
try {
coreSessionToken = await authMessages.decryptPrivateKey(transitKey, coreSessionToken)
} catch (e) {
Logger.info('Failed decryption of coreSessionToken, will try to use as given')
}
}
} else {
throw new LoginFailedError('Authenticating with protocol > 1.1.0 requires transit'
+ ' key, and none found.')
}
}
let hubUrl = BLOCKSTACK_DEFAULT_GAIA_HUB_URL
let gaiaAssociationToken: string
if (isLaterVersion(tokenPayload.version as string, '1.2.0')
&& tokenPayload.hubUrl !== null && tokenPayload.hubUrl !== undefined) {
hubUrl = tokenPayload.hubUrl as string
}
if (isLaterVersion(tokenPayload.version as string, '1.3.0')
&& tokenPayload.associationToken !== null && tokenPayload.associationToken !== undefined) {
gaiaAssociationToken = tokenPayload.associationToken as 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)
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
this.store.setSessionData(sessionData)

return userData
}

/**
Expand All @@ -181,16 +311,6 @@ export class UserSession {
return userData
}


/**
* Sign the user out and optionally redirect to given location.
* @param redirectURL Location to redirect user to after sign out.
* Only used in environments with `window` available
*/
signUserOut(redirectURL?: string) {
signUserOut(redirectURL, this)
}

/**
* Encrypts the data provided with the app public key.
* @param {String|Buffer} content the data to encrypt
Expand Down Expand Up @@ -221,4 +341,19 @@ export class UserSession {
return decryptContent(content, options)
}

/**
* Sign the user out and optionally redirect to given location.
* @param redirectURL
* Location to redirect user to after sign out.
* Only used in environments with `window` available
*/
signUserOut(redirectURL?: string, caller?: UserSession) {
this.store.deleteSessionData()
if (redirectURL) {
getGlobalObject(
'location',
{ throwIfUnavailable: true, usageDesc: 'signUserOut' }
).href = redirectURL
}
}
}

0 comments on commit 3a1dfba

Please sign in to comment.