Skip to content

Commit

Permalink
feat(sdk-middleware-auth): implement anonymous session flow
Browse files Browse the repository at this point in the history
affects: @commercetools/sdk-middleware-auth

resolves #276
  • Loading branch information
hisabimbola authored and wizzy25 committed Aug 22, 2017
1 parent 55a504c commit 30a9f0b
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 82 deletions.
42 changes: 42 additions & 0 deletions docs/sdk/api/sdkMiddlewareAuth.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const client = createClient({
credentials: {
clientId: '123',
clientSecret: 'secret',
user: {
username: string;
password: string;
}
},
scopes: [
'view_products:test',
Expand All @@ -58,6 +62,7 @@ Creates a [middleware](/sdk/Glossary.md#middleware) to handle authentication for

1. `host` *(String)*: the host of the OAuth API service
2. `projectKey` *(String)*: the key of the project to assign the default scope to
- The user field is an object containining `username` and `password`. [Sample below](#usage-example)
3. `credentials` *(Object)*: the client credentials for authentication (`clientId`, `clientSecret`)
4. `scopes` *(Array)*: a list of [scopes](http://dev.commercetools.com/http-api-authorization.html#scopes) to assign to the OAuth token. _No default scope is sent_

Expand Down Expand Up @@ -89,3 +94,40 @@ const client = createClient({
],
})
```

## `createAuthMiddlewareForAnonymousSessionFlow(options)`

Creates a [middleware](/sdk/Glossary.md#middleware) to handle authentication for the [Anonymous Session Flow](http://dev.commercetools.com/http-api-authorization.html#tokens-for-anonymous-sessions) of the commercetools platform API.

#### Named arguments (options)

1. `host` *(String)*: the host of the OAuth API service
2. `projectKey` *(String)*: the key of the project to assign the default scope to
3. `credentials` *(Object)*: the client credentials for authentication (`clientId`, `clientSecret`, `anonymousId`)
4. `scopes` *(Array)*: a list of [scopes](http://dev.commercetools.com/http-api-authorization.html#scopes) (default `manage_project:{projectKey}`) to assign to the OAuth token


#### Usage example

```js
import { createClient } from '@commercetools/sdk-client'
import { createAuthMiddlewareForAnonymousSessionFlow } from '@commercetools/sdk-middleware-auth'

const client = createClient({
middlewares: [
createAuthMiddlewareForAnonymousSessionFlow({
host: 'https://auth.commercetools.com',
projectKey: 'test',
credentials: {
clientId: '123',
clientSecret: 'secret',
anonymousId: 'unique-id-of-customer-not-required',
},
scopes: [
'view_products:test',
'manage_orders:test',
],
}),
],
})
```
123 changes: 123 additions & 0 deletions integration-tests/sdk/auth.it.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { createClient } from '@commercetools/sdk-client'
import { getCredentials } from '@commercetools/get-credentials'
import {
createAuthMiddlewareForPasswordFlow,
createAuthMiddlewareForAnonymousSessionFlow,
} from '@commercetools/sdk-middleware-auth'
import {
createHttpMiddleware,
} from '@commercetools/sdk-middleware-http'
import { clearData, createData } from './../cli/helpers/utils'

let projectKey
if (process.env.CI === 'true')
projectKey = 'auth-integration-test'
else
projectKey = process.env.npm_config_projectkey

describe('Auth Flows', () => {
let apiConfig
const userEmail = `abi${Date.now()}@commercetooler.com`
const userPassword = 'asdifficultaspossible'
beforeAll(() => getCredentials(projectKey)
.then((credentials) => {
apiConfig = {
host: 'https://auth.sphere.io',
apiUrl: 'https://api.sphere.io',
projectKey,
credentials: {
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
},
}
})
.then(() => clearData(apiConfig, 'customers'))
.then(() => clearData(apiConfig, 'carts'))
.then(() => createData(apiConfig, 'customers', [{
email: userEmail,
password: userPassword,
}]))
, 10000)
afterAll(() => {
clearData(apiConfig, 'customers')
.then(() => clearData(apiConfig, 'carts'))
})
describe('Password Session Flow', () => {
const httpMiddleware = createHttpMiddleware({
host: 'https://api.sphere.io',
})

it('should log customer and fetch customer profile', () => {
const userConfig = {
...apiConfig,
...{ scopes: [ `manage_project:${projectKey}` ] },
...{ credentials: {
clientId: apiConfig.credentials.clientId,
clientSecret: apiConfig.credentials.clientSecret,
user: {
username: userEmail,
password: userPassword,
},
} },
}
const client = createClient({
middlewares: [
createAuthMiddlewareForPasswordFlow(userConfig),
httpMiddleware,
],
})
return client.execute({
uri: `/${projectKey}/me`,
method: 'GET',
}).then((response) => {
const user = response.body
expect(user).toHaveProperty('email', userEmail)
})
})
})

describe('Anonymous Session Flow', () => {
const httpMiddleware = createHttpMiddleware({
host: 'https://api.sphere.io',
})

it('create an anonymous session and a cart tied to the session', () => {
const anonymousId = `${Date.now()}-bar`
const userConfig = {
...apiConfig,
...{ scopes: [ `manage_project:${projectKey}` ] },
...{ credentials: {
clientId: apiConfig.credentials.clientId,
clientSecret: apiConfig.credentials.clientSecret,
anonymousId,
} },
}
const client = createClient({
middlewares: [
createAuthMiddlewareForAnonymousSessionFlow(userConfig),
httpMiddleware,
],
})
const cartMock = {
currency: 'EUR',
}
return client.execute({
// creates a cart that is tied to the logged in anonymous token
uri: `/${projectKey}/me/carts`,
method: 'POST',
body: cartMock,
}).then(({ body: cart }) => {
expect(cart).toHaveProperty('anonymousId', anonymousId)
return client.execute({
// fetch all carts tied to the anonymous token, if cart is present,
// then the cart was tied to the token
uri: `/${projectKey}/me/carts`,
method: 'GET',
})
}).then(({ body: { results: carts } }) => {
expect(carts).toHaveLength(1)
expect(carts[0]).toHaveProperty('anonymousId', anonymousId)
})
}, 7000)
})
})
70 changes: 0 additions & 70 deletions integration-tests/sdk/customer-login.it.js

This file was deleted.

37 changes: 35 additions & 2 deletions packages/sdk-middleware-auth/src/anonymous-session-flow.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
export default function createAuthMiddlewareForAnonymousSessionFlow () {
throw new Error('Middleware not implemented yet')
/* @flow */
import type {
AuthMiddlewareOptions,
Middleware,
MiddlewareRequest,
MiddlewareResponse,
Next,
Task,
} from 'types/sdk'

import { buildRequestForAnonymousSessionFlow } from './build-requests'
import authMiddlewareBase from './base-auth-flow'
import store from './utils'

export default function createAuthMiddlewareForAnonymousSessionFlow (
options: AuthMiddlewareOptions,
): Middleware {
const tokenCache = store({})
const pendingTasks: Array<Task> = []

const requestState = store(false)
return (next: Next) => (
request: MiddlewareRequest,
response: MiddlewareResponse,
) => {
const params = {
request,
response,
...buildRequestForAnonymousSessionFlow(options),
pendingTasks,
requestState,
tokenCache,
}
authMiddlewareBase(params, next)
}
}
2 changes: 0 additions & 2 deletions packages/sdk-middleware-auth/src/base-auth-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import type {
Task,
AuthMiddlewareBaseOptions,
} from 'types/sdk'

/* global fetch */
import 'isomorphic-fetch'


export default function authMiddlewareBase ({
request,
response,
Expand Down
20 changes: 17 additions & 3 deletions packages/sdk-middleware-auth/src/build-requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export function buildRequestForPasswordFlow (
const scope = (options.scopes || []).join(' ')
const scopeStr = scope ? `&scope=${scope}` : ''


const basicAuth = new Buffer(`${clientId}:${clientSecret}`).toString('base64')
// This is mostly useful for internal testing purposes to be able to check
// other oauth endpoints.
Expand All @@ -94,6 +93,21 @@ export function buildRequestForRefreshTokenFlow () {
// TODO
}

export function buildRequestForAnonymousSessionFlow () {
// TODO
export function buildRequestForAnonymousSessionFlow (
options: AuthMiddlewareOptions,
): BuiltRequestParams {
if (!options)
throw new Error('Missing required options')

if (!options.projectKey)
throw new Error('Missing required option (projectKey)')
const pKey = options.projectKey
// eslint-disable-next-line no-param-reassign
options.oauthUri = options.oauthUri || `/oauth/${pKey}/anonymous/token`
const result = buildRequestForClientCredentialsFlow(options)

if (options.credentials.anonymousId)
result.body += `&anonymous_id=${options.credentials.anonymousId}`

return { ...result }
}
Loading

2 comments on commit 30a9f0b

@hisabimbola
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed that this commit is a breaking change, but it was not released like that

It's a breaking change because the parameters are now required, whereas before if you pass an authorization header, it's going to skip the parameter validation check.

@wizzy25 how best do you think we handle this

@wizzy25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hisabimbola we can roll it back; skip the validation if authorization header is passed in

Please sign in to comment.