Skip to content
This repository was archived by the owner on May 19, 2023. It is now read-only.

Commit f8d1a76

Browse files
authored
Use DID Auth in cookies mode (#74)
* Improve setup * Partial: set DID Auth cookies in service * Use DID Auth in cookies mode (2) (#80) * Add Access-Control-Allow-Origin header * Update Express DID Auth - move x-csrf-token to headers * Add testing auth manager * Move all requests to auth manager * Rename testing auth manager * Client get working with authentication * Add missing headers * Implement post, put and delete * Refactor requests * Lint * Fix types * Update modules/ipfs-cpinner-service/src/index.ts * Fix service lock * Update package lock * Fix ipfs-http-client version * Unfix ipfs-http-client and ignore type
1 parent eeb6cc6 commit f8d1a76

File tree

20 files changed

+2163
-274
lines changed

20 files changed

+2163
-274
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ Install dependencies
3636
```
3737
npm i
3838
npm run setup
39-
npm run build
4039
```
4140

4241
Install IPFS CLI. Find your option: https://docs.ipfs.io/how-to/command-line-quick-start/.

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module.exports = {
33
testEnvironment: 'node',
44
reporters: ['default', 'jest-junit'],
55
testResultsProcessor: 'jest-junit',
6+
testTimeout: 120000,
67
globals: {
78
'ts-jest': {
89
tsConfig: './modules/tsconfig.settings.json'

modules/ipfs-cpinner-client/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
},
2525
"homepage": "https://github.com/rsksmart/rif-data-vault#readme",
2626
"devDependencies": {
27-
"@rsksmart/ipfs-cpinner-provider": "0.1.1-beta.9",
28-
"@rsksmart/ipfs-cpinner-service": "0.1.1-beta.13",
27+
"@rsksmart/ipfs-cpinner-provider": "0.1.1",
28+
"@rsksmart/ipfs-cpinner-service": "0.1.1",
2929
"@rsksmart/rif-id-ethr-did": "^0.1.0",
3030
"@rsksmart/rif-id-mnemonic": "^0.1.0",
3131
"@types/axios": "^0.14.0",
Lines changed: 60 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,90 @@
11
import axios from 'axios'
2-
import { decodeJWT } from 'did-jwt'
3-
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from './constants'
2+
import { IAuthManager, DIDAuthConfig, KeyValueStore, PersonalSign } from './types'
43
import { LocalStorage } from './store'
5-
import { LoginResponse, DIDAuthConfig, PersonalSign, KeyValueStore, DIDAuthStoreConfig, DIDAuthServiceConfig } from './types'
6-
import { Web3Provider } from '../web3provider/types'
74

8-
class AuthManager {
5+
const xCsrfToken = 'x-csrf-token'
6+
7+
class AuthManager implements IAuthManager {
98
store: KeyValueStore
109
did: string
1110
serviceUrl: string
1211
personalSign: PersonalSign
1312

14-
constructor (config: DIDAuthConfig) {
15-
this.store = config.store || new LocalStorage()
16-
this.did = config.did
17-
this.serviceUrl = config.serviceUrl
18-
this.personalSign = config.personalSign
13+
constructor ({ store, did, serviceUrl, personalSign }: DIDAuthConfig) {
14+
this.store = store || new LocalStorage()
15+
this.did = did
16+
this.serviceUrl = serviceUrl
17+
this.personalSign = personalSign
18+
19+
axios.defaults.withCredentials = true
1920
}
2021

21-
// store
22-
private storeTokens = async ({ accessToken, refreshToken }: { accessToken: string, refreshToken: string }) => {
23-
await Promise.all([
24-
this.store.set(`${ACCESS_TOKEN_KEY}-${this.did}`, accessToken),
25-
this.store.set(`${REFRESH_TOKEN_KEY}-${this.did}`, refreshToken)
26-
])
22+
private saveCsrf = async (response) => {
23+
const token = await this.store.get(xCsrfToken)
2724

28-
return { accessToken, refreshToken }
29-
}
25+
if (!token) {
26+
await this.store.set(xCsrfToken, response.headers[xCsrfToken])
27+
}
3028

31-
private getStoredAccessToken = () => this.store.get(`${ACCESS_TOKEN_KEY}-${this.did}`)
32-
private getStoredRefreshToken = () => this.store.get(`${REFRESH_TOKEN_KEY}-${this.did}`)
29+
return response
30+
}
3331

3432
// did auth challenge-response authentication
3533
private getChallenge = (): Promise<string> => axios.get(`${this.serviceUrl}/request-auth/${this.did}`)
36-
.then(res => res.status === 200 && !!res.data && res.data.challenge)
34+
.catch(e => this.saveCsrf(e.response))
35+
.then(res => {
36+
this.store.set(xCsrfToken, res.headers[xCsrfToken])
37+
return res.data.challenge
38+
})
39+
.then(this.saveCsrf)
3740

3841
private signChallenge = (challenge: string) => this.personalSign(
3942
`Are you sure you want to login to the RIF Data Vault?\nURL: ${this.serviceUrl}\nVerification code: ${challenge}`
4043
).then(sig => ({ did: this.did, sig }))
4144

42-
private login = (): Promise<LoginResponse> => this.getChallenge()
43-
.then(this.signChallenge)
44-
.then(signature => axios.post(`${this.serviceUrl}/auth`, { response: signature }))
45-
.then(res => res.status === 200 && res.data)
46-
.then(this.storeTokens)
47-
48-
private async refreshAccessToken (): Promise<LoginResponse> {
49-
const refreshToken = await this.getStoredRefreshToken()
50-
51-
if (!refreshToken) return this.login()
52-
53-
return axios.post(`${this.serviceUrl}/refresh-token`, { refreshToken })
54-
.then(res => res.status === 200 && res.data)
55-
.then(this.storeTokens)
56-
.catch(err => {
57-
if (err.response.status !== 401) throw err
58-
59-
// if it is expired, do another login
60-
return this.login()
61-
})
62-
}
63-
64-
// api
65-
public async getAccessToken () {
66-
const accessToken = await this.getStoredAccessToken()
67-
68-
if (!accessToken) return this.login().then(tokens => tokens.accessToken)
45+
private login = (): Promise<void> => this.store.get(xCsrfToken)
46+
.then(token => this.getChallenge()
47+
.then(this.signChallenge)
48+
.then(signature => axios.post(`${this.serviceUrl}/auth`, { response: signature }, {
49+
headers: { 'x-csrf-token': token }
50+
})))
51+
52+
private getConfig = () => this.store.get(xCsrfToken).then(token => ({
53+
headers: {
54+
'x-csrf-token': token,
55+
'x-logged-did': this.did
56+
}
57+
}))
6958

70-
// TODO: should we verify?
71-
const { payload } = decodeJWT(accessToken)
59+
private async refreshAccessToken (): Promise<void> {
60+
const config = await this.getConfig()
7261

73-
if (payload.exp <= Math.floor(Date.now() / 1000)) {
74-
return this.refreshAccessToken().then(tokens => tokens.accessToken)
62+
try {
63+
await axios.post(`${this.serviceUrl}/refresh-token`, {}, config)
64+
} catch (e) {
65+
if (e.response.status !== 401) throw e
66+
await this.login()
7567
}
76-
77-
return accessToken
7868
}
7969

80-
public storedTokens = () => Promise.all([
81-
this.getStoredAccessToken(),
82-
this.getStoredRefreshToken()
83-
]).then(([accessToken, refreshToken]) => ({
84-
accessToken,
85-
refreshToken
86-
}))
87-
88-
static fromWeb3Provider (config: DIDAuthServiceConfig & DIDAuthStoreConfig, provider: Web3Provider) {
89-
return provider.request({
90-
method: 'eth_accounts'
91-
}).then(accounts => new AuthManager({
92-
...config,
93-
personalSign: (data: string) => provider.request({
94-
method: 'personal_sign',
95-
params: [data, accounts[0]]
70+
private request = (method: any) => async (...args) => {
71+
const config = await this.getConfig()
72+
return await method(config, ...args)
73+
.then(r => r as any)
74+
.catch(e => {
75+
if (e.response.status === 401) {
76+
return this.saveCsrf(e.response)
77+
.then(() => this.refreshAccessToken())
78+
.then(() => method(config, ...args))
79+
}
80+
throw e
9681
})
97-
}))
9882
}
83+
84+
get: typeof axios.get = this.request((config, ...args) => axios.get(args[0], config))
85+
post: typeof axios.post = this.request((config, ...args) => axios.post(args[0], args[1], config))
86+
put: typeof axios.put = this.request((config, ...args) => axios.put(args[0], args[1], config))
87+
delete: typeof axios.delete = this.request((config, ...args) => axios.delete(args[0], config))
9988
}
10089

10190
export default AuthManager
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import axios from 'axios'
2+
import { decodeJWT } from 'did-jwt'
3+
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from './constants'
4+
import { LocalStorage } from './store'
5+
import { IAuthManager, LoginResponse, DIDAuthConfig, PersonalSign, KeyValueStore, DIDAuthStoreConfig, DIDAuthServiceConfig } from './types'
6+
import { Web3Provider } from '../web3provider/types'
7+
8+
class AuthManager implements IAuthManager {
9+
store: KeyValueStore
10+
did: string
11+
serviceUrl: string
12+
personalSign: PersonalSign
13+
csrfHeader: string
14+
cookies: string[]
15+
16+
constructor (config: DIDAuthConfig) {
17+
this.store = config.store || new LocalStorage()
18+
this.did = config.did
19+
this.serviceUrl = config.serviceUrl
20+
this.personalSign = config.personalSign
21+
}
22+
23+
// store
24+
private storeTokens = async ({ accessToken, refreshToken }: { accessToken: string, refreshToken: string }) => {
25+
await Promise.all([
26+
this.store.set(`${ACCESS_TOKEN_KEY}-${this.did}`, accessToken),
27+
this.store.set(`${REFRESH_TOKEN_KEY}-${this.did}`, refreshToken)
28+
])
29+
30+
return { accessToken, refreshToken }
31+
}
32+
33+
private getStoredAccessToken = () => this.store.get(`${ACCESS_TOKEN_KEY}-${this.did}`)
34+
private getStoredRefreshToken = () => this.store.get(`${REFRESH_TOKEN_KEY}-${this.did}`)
35+
36+
// did auth challenge-response authentication
37+
private getChallenge = (): Promise<string> => axios.get(`${this.serviceUrl}/request-auth/${this.did}`)
38+
.then(res => {
39+
if (!(res.status === 200 && !!res.data)) throw new Error('Invalid response')
40+
41+
this.csrfHeader = res.headers['x-csrf-token']
42+
this.cookies = res.headers['set-cookie'].map(cookie => cookie.split(';')[0])
43+
44+
return res.data.challenge
45+
})
46+
47+
private signChallenge = (challenge: string) => this.personalSign(
48+
`Are you sure you want to login to the RIF Data Vault?\nURL: ${this.serviceUrl}\nVerification code: ${challenge}`
49+
).then(sig => ({ did: this.did, sig }))
50+
51+
private login = (): Promise<LoginResponse> => this.getChallenge()
52+
.then(this.signChallenge)
53+
.then(signature => axios.post(`${this.serviceUrl}/auth`, { response: signature }, {
54+
headers: {
55+
'x-csrf-token': this.csrfHeader,
56+
cookie: this.cookies
57+
}
58+
}))
59+
.then(res => res.status === 200 && {
60+
accessToken: res.headers['set-cookie'][0].split(';')[0].split('=')[1],
61+
refreshToken: res.headers['set-cookie'][1].split(';')[0].split('=')[1]
62+
})
63+
.then(this.storeTokens)
64+
65+
private async refreshAccessToken (): Promise<LoginResponse> {
66+
const refreshToken = await this.getStoredRefreshToken()
67+
68+
if (!refreshToken) return this.login()
69+
70+
return axios.post(`${this.serviceUrl}/refresh-token`, {}, {
71+
headers: {
72+
'x-csrf-token': this.csrfHeader,
73+
cookie: `refresh-token-${this.did}=${refreshToken};${this.cookies[0]}`
74+
}
75+
})
76+
.then(res => res.status === 200 && res.data)
77+
.then(this.storeTokens)
78+
.catch(err => {
79+
if (err.response.status !== 401) throw err
80+
81+
// if it is expired, do another login
82+
return this.login()
83+
})
84+
}
85+
86+
// api
87+
public async getAccessToken () {
88+
const accessToken = await this.getStoredAccessToken()
89+
90+
if (!accessToken) return this.login().then(tokens => tokens.accessToken)
91+
92+
// TODO: should we verify?
93+
const { payload } = decodeJWT(accessToken)
94+
95+
if (payload.exp <= Math.floor(Date.now() / 1000)) {
96+
return this.refreshAccessToken().then(tokens => tokens.accessToken)
97+
}
98+
99+
return accessToken
100+
}
101+
102+
public storedTokens = () => Promise.all([
103+
this.getStoredAccessToken(),
104+
this.getStoredRefreshToken()
105+
]).then(([accessToken, refreshToken]) => ({
106+
accessToken,
107+
refreshToken
108+
}))
109+
110+
public getHeaders = () => this.getAccessToken()
111+
.then(accessToken => ({
112+
'x-logged-did': this.did,
113+
'x-csrf-token': this.csrfHeader,
114+
cookie: `authorization-${this.did}=${accessToken};${this.cookies[0]}`
115+
}))
116+
117+
public get: typeof axios.get = (...args) => this.getHeaders()
118+
.then(headers => axios.get(args[0], { headers }))
119+
120+
public post: typeof axios.post = (...args) => this.getHeaders()
121+
.then(headers => axios.post(args[0], args[1], { headers }))
122+
123+
public delete: typeof axios.delete = (...args) => this.getHeaders()
124+
.then(headers => axios.delete(args[0], { headers }))
125+
126+
public put: typeof axios.put = (...args) => this.getHeaders()
127+
.then(headers => axios.put(args[0], args[1], { headers }))
128+
129+
static fromWeb3Provider (config: DIDAuthServiceConfig & DIDAuthStoreConfig, provider: Web3Provider) {
130+
return provider.request({
131+
method: 'eth_accounts'
132+
}).then(accounts => new AuthManager({
133+
...config,
134+
personalSign: (data: string) => provider.request({
135+
method: 'personal_sign',
136+
params: [data, accounts[0]]
137+
})
138+
}))
139+
}
140+
}
141+
142+
export default AuthManager

modules/ipfs-cpinner-client/src/auth-manager/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import axios from 'axios'
2+
13
export type LoginResponse = { accessToken: string, refreshToken: string }
24

35
export type PersonalSign = (data: string) => Promise<string>
@@ -23,3 +25,10 @@ export type DIDAuthStoreConfig = {
2325
}
2426

2527
export type DIDAuthConfig = DIDAuthServiceConfig & DIDAuthClientConfig & DIDAuthStoreConfig
28+
29+
export interface IAuthManager {
30+
get: typeof axios.get
31+
post: typeof axios.post
32+
delete: typeof axios.delete
33+
put: typeof axios.put
34+
}

0 commit comments

Comments
 (0)