Comparing version 0.1.2 to 0.1.3
{ | ||
"name": "lfs-api", | ||
"version": "0.1.2", | ||
"type": "module", | ||
"description": "Query the Live for Speed API with NodeJS", | ||
"main": "dist/index.js", | ||
"version": "0.1.3", | ||
"description": "Query the Live for Speed OAuth2 API in your Web projects", | ||
"main": "dist/cjs/index.js", | ||
"module": "dist/esm/index.js", | ||
"types": "dist/types/index.d.ts", | ||
"repository": { | ||
@@ -18,9 +19,14 @@ "type": "git", | ||
"scripts": { | ||
"dist": "tsc --declaration -p ./" | ||
"build": "yarn build:cjs && yarn build:esm && yarn build:types", | ||
"build:cjs": "tsc --outDir dist/cjs --module commonjs", | ||
"build:esm": "tsc --outDir dist/esm --module es2015", | ||
"build:types": "tsc --outDir dist/types --emitDeclarationOnly --declaration" | ||
}, | ||
"dependencies": { | ||
"node-fetch": "^3.0.0", | ||
"axios": "^0.24.0", | ||
"js-sha256": "^0.9.0", | ||
"uuid": "^8.3.2" | ||
}, | ||
"devDependencies": { | ||
"@tsconfig/node14": "^1.0.1", | ||
"@types/node": "^16.11.0", | ||
@@ -27,0 +33,0 @@ "@types/uuid": "^8.3.1", |
189
README.md
@@ -5,6 +5,4 @@ # lfs-api | ||
Query the [Live for Speed](https://lfs.net) API in your projects. | ||
Query the [Live for Speed](https://lfs.net) OAuth2 API in your Web projects. | ||
**Note:** This package does not yet support authorization flows with PKCE and is therefore only suitable for secure applications at this point in time. Do not use this module in insecure or single page applications (SPAs). | ||
--- | ||
@@ -18,3 +16,3 @@ | ||
## Usage | ||
## Supported Flows | ||
@@ -25,17 +23,25 @@ Before you can make requests, you need to [register an application](https://lfs.net/account/api) with the LFS API. Next, choose the most appropriate LFS API OAuth2 flow for your project of those supported. | ||
Your application can securely store a secret. The source code is hidden from the public. | ||
Secure applications are defined as follows: | ||
> Your application can securely store a secret. The source code is hidden from the public. | ||
- [Client Credentials Flow with Client Secret](#client-credentials-flow-with-client-secret) | ||
- [Authorization Flow with Client Secret](#authorization-flow-with-client-secret) | ||
### Single Page Applications (SPAs) | ||
### Insecure Applications | ||
**Coming soon!** | ||
Insecure applications are defined as follows: | ||
Your application cannot securely store a secret because all source code is public domain. | ||
> Your application cannot securely store a secret because all source code is public domain E.g. Single Page Applications (SPAs). | ||
- [Authorization flow with PKCE](#authorization-flow-with-pkce) | ||
--- | ||
## Usage | ||
### Client Credentials Flow with Client Secret | ||
[`Secure apps only`](#secure-applications) | ||
You can use your `client_id` and `client_secret` to make LFS API calls in your app: | ||
@@ -63,10 +69,14 @@ | ||
This flow allows you to authenticate a user with their LFS account. | ||
[`Secure apps only`](#secure-applications) | ||
- First you will generate a URL including your `client_id`, `scope`, `redirect_uri` and an optional CSRF token (`state`). The `redirect_uri` is set in your [lfs.net](https://lfs.net) API settings (can be localhost for development and testing). | ||
- When a user clicks the URL they are directed to [lfs.net](https://lfs.net) to authenticate. | ||
- Once a user has accepted the scope, they are returned to your site's `redirect_uri`. | ||
- This user is now authenticated. To identify the user, and to make API queries, you will now need to request access tokens using the autorization code in the URL query string. | ||
This flow allows you to authenticate a user with their LFS account and make API requests based on the accepted OAuth2 scopes. | ||
- Pass your `client_id`, `client_secret` and `redirect_uri` to the constructor. | ||
- Next you will generate a URL to direct users to [lfs.net](https://lfs.net) to accept OAuth2 scopes. Pass a space separated list of scopes to `LFSAPI.generateAuthFlowURL(scope)`. | ||
- Once a user clicks this URL and accepts the OAuth2 scopes, the user will be redirected to the `redirect_uri` you set in your [lfs.net API settings](https://lfs.net/account/api) (can be localhost for development and testing). | ||
- This user is now authenticated. To identify the user, and to make calls to the API, you will now need to request access tokens using the autorization code in the URL query string with `LFSAPI.getAuthFlowTokens(code)`. | ||
- Once these tokens have been received, you will get access to some protected API endpoints based on the accepted scope as well as all other API endpoints. | ||
You can refresh tokens with `LFSAPI.refreshAccessTokens(refreshToken)`. | ||
Here's a short example: | ||
@@ -124,4 +134,120 @@ | ||
This package does not yet support authorization flow with PKCE. | ||
This flow allows you to authenticate a user with their LFS account and make API requests based on the accepted scopes in insecure applications E.g. Single Page Apps (SPAs). | ||
- Pass your `client_id`, `client_secret` and `redirect_uri` to the constructor. The 4th argument (`spa`) should be set to `true` to mark this app as an insecure app. | ||
- Next you will generate a URL to direct users to [lfs.net](https://lfs.net) to accept OAuth2 scopes. Pass a space separated list of scopes to `LFSAPI.generateAuthFlowURL(scope)`. | ||
- At this point you will need to store the PKCE challenge verifier in a cookie (or local/session storage) to use after a user is redirected back to your app. You can get the verifier using `LFSAPI.getPKCEVerifier()`. | ||
- Once a user clicks the URL you generated earlier and accepts the OAuth2 scopes, the user will be redirected to the `redirect_uri` you set in your [lfs.net API settings](https://lfs.net/account/api) (can be localhost for development and testing). | ||
- This user is now authenticated. To identify the user, and to make calls to the API: | ||
- First, you will need to let `lfs-api` know about the PKCE challenge verifier you stored earlier with `LFSAPI.setPKCEVerifier(codeVerifier)`. | ||
- You can now request access tokens using the autorization code in the URL query string with `LFSAPI.getAuthFlowTokens(code)` | ||
- Once these tokens have been received, you will get access to some protected API endpoints based on the accepted scope as well as all other API endpoints. | ||
**Remember:** Don't forget to remove the PKCE code verifier cookie or session/localstorage object. | ||
Here's a short example using [React](https://reactjs.org): | ||
```jsx | ||
import React, { useEffect, useState } from "react"; | ||
import ReactDOM from "react-dom"; | ||
import { | ||
BrowserRouter as Router, | ||
Routes, | ||
Route, | ||
useSearchParams, | ||
} from "react-router-dom"; | ||
import Cookies from "js-cookie"; | ||
import LFSAPI from "lfs-api"; | ||
import { CLIENT_ID, CLIENT_SECRET } from "./secrets"; | ||
const api = new LFSAPI( | ||
CLIENT_ID, | ||
CLIENT_SECRET, | ||
"http://localhost:3000/", | ||
true // Marks this app as an SPA | ||
); | ||
function App() { | ||
return ( | ||
<Router> | ||
<Routes> | ||
<Route path="/" element={<Home />} /> | ||
<Route path="*" element={<h1>404</h1>} /> | ||
</Routes> | ||
</Router> | ||
); | ||
} | ||
function Home() { | ||
const [searchParams] = useSearchParams(); | ||
const [authURL, setAuthURL] = useState(""); | ||
const [user, setUser] = useState(); | ||
useEffect(() => { | ||
async function authFlow() { | ||
// Generate auth flow URL | ||
const authFlowURL = await api.generateAuthFlowURL("openid profile email"); | ||
// Code Verifier Cookie Exists | ||
if (Cookies.get("codeVerifier")) { | ||
// Set verifier if exists in cookie | ||
api.setPKCEVerifier(Cookies.get("codeVerifier")); | ||
} | ||
// If code and no cookie... | ||
if (!Cookies.get("accessToken") && searchParams.get("code")) { | ||
const authFlowTokens = await api.getAuthFlowTokens( | ||
searchParams.get("code") | ||
); | ||
// Set cookies | ||
Cookies.set("accessToken", authFlowTokens.access_token); | ||
Cookies.set("refreshToken", authFlowTokens.refresh_token); | ||
Cookies.set("expires", Date.now() / 1000 + authFlowTokens.expires_in); | ||
// Remove invalid code verifier cookie | ||
Cookies.remove("codeVerifier"); | ||
} | ||
// If code verifier cookie hasn't been set yet, set it | ||
if (!Cookies.get("codeVerifier")) { | ||
Cookies.set("codeVerifier", api.getPKCEVerifier()); | ||
} | ||
if (Cookies.get("accessToken")) { | ||
if (Cookies.get("expires") < Date.now() / 1000) { | ||
// Access token expired! | ||
// Check for expiration and get new tokens with refresh token... | ||
} | ||
// Use protected APIs... | ||
const user = await api.getUserInfo(Cookies.get("accessToken")); | ||
setUser(user); | ||
} | ||
setAuthURL(authFlowURL.authURL); | ||
} | ||
authFlow(); | ||
}, [searchParams]); | ||
return ( | ||
<> | ||
{user ? ( | ||
`Welcome ${user.data.name}!` | ||
) : ( | ||
<a href={authURL}>Authenticate with lfs.net</a> | ||
)} | ||
</> | ||
); | ||
} | ||
ReactDOM.render( | ||
<React.StrictMode> | ||
<App /> | ||
</React.StrictMode>, | ||
document.getElementById("root") | ||
); | ||
``` | ||
This example doesn't handle refreshing a token, that is left as an exercise for the reader. You can refresh tokens with `LFSAPI.refreshAccessTokens(refreshToken)`. | ||
--- | ||
@@ -131,3 +257,3 @@ | ||
### `LFSAPI.constructor(client_id, client_secret, [redirect_url])` | ||
### `LFSAPI.constructor(client_id, client_secret, [redirect_url], [spa])` | ||
@@ -138,7 +264,8 @@ Create an LFS API class instance. | ||
| Parameter | Type | Description | | ||
| --------------- | -------- | --------------------------------------------------------- | | ||
| `client_id` | _string_ | Your application's client ID | | ||
| `client_secret` | _string_ | Your application's client secret | | ||
| `redirect_uri` | _string_ | Your application's redirect URI (Use only with Auth Flow) | | ||
| Parameter | Type | Description | | ||
| --------------- | --------- | --------------------------------------------------------------------- | | ||
| `client_id` | _string_ | Your application's client ID | | ||
| `client_secret` | _string_ | Your application's client secret | | ||
| `redirect_uri` | _string_ | Your application's redirect URI (Use only with Auth Flow) | | ||
| `spa` | _boolean_ | Mark this application as insecure (Use only with Auth Flow with PKCE) | | ||
@@ -227,2 +354,4 @@ #### Example | ||
When using auth flow with PKCE, you _must_ call [`LFSAPI.setPKCEVerifier(code_verifier)`](#lfsapisetpkceverifiercodeverifier) beforehand. | ||
#### Parameters | ||
@@ -244,2 +373,16 @@ | ||
### `LFSAPI.getPKCEVerifier()` | ||
Returns the code verifier part of the PKCE challenge pair. This should be used to set a cookie or LocalStorage entry during authorization flows with PKCE. | ||
### `LFSAPI.setPKCEVerifier(code_verifier)` | ||
Used to set the code verifier part of the PKCE challenge pair. This should be used to let `lfs-api` know what the current code verifier is once users are redirected back to your `redirect_uri` after scope confirmation. You should call this function before requesting access tokens. | ||
#### Parameters | ||
| Parameter | Type | Description | | ||
| --------------- | -------- | ----------------------------------------------------------------------------------- | | ||
| `code_verifier` | _string_ | A PKCE code verifier retrieved from a cookie or storage prior to scope confirmation | | ||
--- | ||
@@ -378,1 +521,7 @@ | ||
`verbose` _boolean_ - Verbose debug messages | ||
--- | ||
## Limitations | ||
The PKCE flow is quite an involved process. It would be better if the library used `js-cookie` or `LocalStorage` natively to _optionally_ handle setting, removing and updating access tokens, refresh tokens and the PKCE code verifier from cookies/storage automatically. This is a planned update. |
198
src/index.ts
@@ -1,9 +0,5 @@ | ||
import fetch from "node-fetch"; | ||
import axios from "axios"; | ||
import { v4 as uuidv4 } from "uuid"; | ||
import { sha256 } from "js-sha256"; | ||
type CCFlow = { | ||
access_token: string; | ||
expires_in: number; | ||
}; | ||
/** | ||
@@ -20,3 +16,6 @@ * @name LFSAPI | ||
apiURL: string; | ||
client_credentials_flow: CCFlow; | ||
client_credentials_flow: { | ||
access_token: string | null; | ||
expires_in: number; | ||
}; | ||
client_id: string; | ||
@@ -26,2 +25,7 @@ client_secret: string; | ||
redirect_uri?: string; | ||
spa?: boolean; | ||
pkce: { | ||
code_verifier: string | null; | ||
code_challenge: string | null; | ||
}; | ||
verbose: boolean; | ||
@@ -34,2 +38,3 @@ version: string; | ||
redirect_uri?: string, | ||
spa?: boolean, | ||
idURL?: string, | ||
@@ -50,2 +55,11 @@ apiURL?: string | ||
// Single Page Application | ||
this.spa = spa; | ||
// Code Challenge | ||
this.pkce = { | ||
code_verifier: null, | ||
code_challenge: null, | ||
}; | ||
// ID Endpoint | ||
@@ -85,12 +99,22 @@ this.idURL = idURL ? idURL : "https://id.lfs.net"; | ||
*/ | ||
generateAuthFlowURL(scope: string, state?: string) { | ||
async generateAuthFlowURL(scope: string, state?: string) { | ||
const csrfToken = state ? state : uuidv4(); | ||
const authURLParams = new URLSearchParams({ | ||
response_type: "code", | ||
client_id: this.client_id, | ||
redirect_uri: this.redirect_uri, | ||
scope, | ||
state: csrfToken, | ||
}); | ||
const authURLParams = new URLSearchParams(); | ||
// Mutual params | ||
authURLParams.set("response_type", "code"); | ||
authURLParams.set("client_id", this.client_id); | ||
authURLParams.set("redirect_uri", this.redirect_uri); | ||
authURLParams.set("scope", scope); | ||
authURLParams.set("state", csrfToken); | ||
// Generate PKCE Challenge Pair | ||
if (this.spa) await this._generatePKCEPair(); | ||
if (this.spa && this.redirect_uri && this.pkce.code_challenge) { | ||
// URLSearchParams for auth flow with pkce | ||
authURLParams.set("code_challenge", this.pkce.code_challenge); | ||
authURLParams.set("code_challenge_method", "S256"); | ||
} | ||
if (this.redirect_uri) { | ||
@@ -141,5 +165,7 @@ // Secure applications with Authorization Flow | ||
* @param access_token_expiry_store Access token type expiry | ||
* @returns Access Tokens | ||
*/ | ||
async _getAccessToken(params: URLSearchParams) { | ||
return await fetch(`${this.idURL}/oauth2/access_token`, { | ||
return await axios({ | ||
url: `${this.idURL}/oauth2/access_token`, | ||
method: "POST", | ||
@@ -149,6 +175,7 @@ headers: { | ||
}, | ||
body: params, | ||
data: params, | ||
}) | ||
.then((res: { json: () => any }) => res.json()) | ||
.then((json) => json) | ||
.then((res) => { | ||
return res.data; | ||
}) | ||
.catch((err) => { | ||
@@ -165,29 +192,44 @@ this._error(err); | ||
* @param {string} code - Authorization code returned from LFS auth server | ||
* @returns Access tokens | ||
*/ | ||
async getAuthFlowTokens(code: string) { | ||
const params = new URLSearchParams({ | ||
grant_type: "authorization_code", | ||
client_id: this.client_id, | ||
client_secret: this.client_secret, | ||
redirect_uri: this.redirect_uri, | ||
code, | ||
}); | ||
const authFlowTokenParams = new URLSearchParams(); | ||
return await this._getAccessToken(params); | ||
// Mutual params | ||
authFlowTokenParams.set("grant_type", "authorization_code"); | ||
authFlowTokenParams.set("client_id", this.client_id); | ||
authFlowTokenParams.set("redirect_uri", this.redirect_uri); | ||
authFlowTokenParams.set("code", code); | ||
if (this.spa && this.redirect_uri && this.pkce.code_verifier) { | ||
// Auth flow with PKCE | ||
authFlowTokenParams.set("code_verifier", this.pkce.code_verifier); | ||
} else if (this.redirect_uri) { | ||
// Auth flow with client secret | ||
authFlowTokenParams.set("client_secret", this.client_secret); | ||
} | ||
return await this._getAccessToken(authFlowTokenParams); | ||
} | ||
/** | ||
* @private | ||
* @public | ||
* @name refreshAccessToken | ||
* @description Refresh an access token using refresh token | ||
* @param {string} refresh_token - Refresh token | ||
* @returns Access tokens | ||
*/ | ||
async refreshAccessToken(refresh_token: string) { | ||
const params = new URLSearchParams({ | ||
grant_type: "refresh_token", | ||
refresh_token, | ||
client_id: this.client_id, | ||
client_secret: this.client_secret, | ||
}); | ||
const refreshTokenParams = new URLSearchParams(); | ||
return await this._getAccessToken(params); | ||
// Mutual params | ||
refreshTokenParams.set("grant_type", "refresh_token"); | ||
refreshTokenParams.set("refresh_token", refresh_token); | ||
refreshTokenParams.set("client_id", this.client_id); | ||
if (!this.spa) { | ||
// Refresh token params for auth flow with client secret | ||
refreshTokenParams.set("client_secret", this.client_secret); | ||
} | ||
return await this._getAccessToken(refreshTokenParams); | ||
} | ||
@@ -212,3 +254,4 @@ | ||
// Make API request | ||
return await fetch(`${this.apiURL}/${endpoint}`, { | ||
return await axios({ | ||
url: `${this.apiURL}/${endpoint}`, | ||
method: "GET", | ||
@@ -221,7 +264,8 @@ headers: { | ||
}) | ||
.then((res: { json: () => any }) => res.json()) | ||
.then((json: any) => json) | ||
.then((res) => { | ||
return res.data; | ||
}) | ||
.catch((err: any) => { | ||
this._error(err); | ||
return err; | ||
return err?.response ? err.response.data : err; | ||
}); | ||
@@ -235,2 +279,3 @@ } | ||
* @param {boolean} v | ||
* @returns this | ||
*/ | ||
@@ -253,2 +298,77 @@ setVerbose(v: boolean) { | ||
/** | ||
* @private | ||
* @name _generateCodeVerifier | ||
* @description Generate verifier for PKCE challenge pair | ||
* @returns PKCE Challenge verifier | ||
*/ | ||
_generateCodeVerifier() { | ||
function dec2hex(dec: { toString: (arg0: number) => string }) { | ||
return ("0" + dec.toString(16)).substr(-2); | ||
} | ||
var array = new Uint32Array(56 / 2); | ||
window.crypto.getRandomValues(array); | ||
return Array.from(array, dec2hex).join(""); | ||
} | ||
/** | ||
* @private | ||
* @name _generateCodeChallengeFromVerifier | ||
* @description Generate code challenge for PKCE challenge pair | ||
* @param {string} verifier - PKCE Challenge verifier | ||
* @returns PKCE Challenge code challenge | ||
*/ | ||
async _generateCodeChallengeFromVerifier(verifier: string) { | ||
return btoa( | ||
String.fromCharCode( | ||
...new Uint8Array( | ||
new Uint8Array( | ||
sha256(verifier) | ||
.match(/.{1,2}/g) | ||
.map((byte) => parseInt(byte, 16)) | ||
) | ||
) | ||
) | ||
) | ||
.slice(0, -1) | ||
.replace(/[+]/g, "-") | ||
.replace(/\//g, "_"); | ||
} | ||
/** | ||
* @public | ||
* @name getPKCEVerifier | ||
* @description Get PKCE Challenge verifier | ||
* @returns PKCE Challenge verifier | ||
*/ | ||
getPKCEVerifier() { | ||
return this.pkce.code_verifier; | ||
} | ||
/** | ||
* @public | ||
* @name setPKCEVerifier | ||
* @description Set PKCE Challenge verifier | ||
* @param {string} code_verifier - PKCE Challenge code verifier | ||
*/ | ||
setPKCEVerifier(code_verifier: string) { | ||
this.pkce.code_verifier = code_verifier; | ||
} | ||
async _generatePKCEPair() { | ||
// SPA: Generate code verifier and challenge | ||
if (this.spa) { | ||
let codeVerifier = this._generateCodeVerifier(); | ||
let codeChallenge = await this._generateCodeChallengeFromVerifier( | ||
codeVerifier | ||
); | ||
this.pkce = { | ||
code_verifier: codeVerifier, | ||
code_challenge: codeChallenge, | ||
}; | ||
} | ||
} | ||
// ENDPOINTS | ||
@@ -255,0 +375,0 @@ |
{ | ||
"extends": "@tsconfig/node14/tsconfig.json", | ||
"compilerOptions": { | ||
"target": "es2017", | ||
"rootDir": "./src/", | ||
"outDir": "./dist/", | ||
"target": "ES2015", | ||
"strict": false, | ||
"lib": ["dom"], | ||
"moduleResolution": "node" | ||
} | ||
}, | ||
"exclude": ["dist"] | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
78169
7
1762
518
1
3
4
No
+ Addedaxios@^0.24.0
+ Addedjs-sha256@^0.9.0
+ Addedaxios@0.24.0(transitive)
+ Addedfollow-redirects@1.15.9(transitive)
+ Addedjs-sha256@0.9.0(transitive)
- Removednode-fetch@^3.0.0
- Removeddata-uri-to-buffer@4.0.1(transitive)
- Removedfetch-blob@3.2.0(transitive)
- Removedformdata-polyfill@4.0.10(transitive)
- Removednode-domexception@1.0.0(transitive)
- Removednode-fetch@3.3.2(transitive)
- Removedweb-streams-polyfill@3.3.3(transitive)