Skip to content

Commit

Permalink
feat: add sdk for web front-only app (#47)
Browse files Browse the repository at this point in the history
* feat: add sdk for web front-only app

* feat: update README.md
  • Loading branch information
leo220yuyaodog authored Sep 14, 2023
1 parent 3ac4ed3 commit e25d27d
Show file tree
Hide file tree
Showing 4 changed files with 1,560 additions and 87 deletions.
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,72 @@ popupSignin(serverUrl, signinPath)
````
Popup a window to handle the callback url from casdoor, call the back-end api to complete the login process and store the token in localstorage, then reload the main window. See Demo: [casdoor-nodejs-react-example](https://github.com/casdoor/casdoor-nodejs-react-example).

### OAuth2 PKCE flow sdk (for SPA without backend)

#### Start the authorization process

Typically, you just need to go to the authorization url to start the process. This example is something that might work in an SPA.

```typescript
signin_redirect();
```

You may add additional query parameters to the authorize url by using an optional second parameter:

```typescript
const additionalParams = {test_param: 'testing'};
signin_redirect(additionalParams);
```

#### Trade the code for a token

When you get back here, you need to exchange the code for a token.

```typescript
sdk.exchangeForAccessToken().then((resp) => {
const token = resp.access_token;
// Do stuff with the access token.
});
```

As with the authorizeUrl method, an optional second parameter may be passed to the exchangeForAccessToken method to send additional parameters to the request:

```typescript
const additionalParams = {test_param: 'testing'};

sdk.exchangeForAccessToken(additionalParams).then((resp) => {
const token = resp.access_token;
// Do stuff with the access token.
});
```

#### Get user info

Once you have an access token, you can use it to get user info.

```typescript
getUserInfo(accessToken).then((resp) => {
const userInfo = resp;
// Do stuff with the user info.
});
```

#### A note on Storage
By default, this package will use sessionStorage to persist the pkce_state. On (mostly) mobile devices there's a higher chance users are returning in a different browser tab. E.g. they kick off in a WebView & get redirected to a new tab. The sessionStorage will be empty there.

In this case it you can opt in to use localStorage instead of sessionStorage:

```typescript
import {SDK, SdkConfig} from 'casdoor-js-sdk'

const sdkConfig = {
// ...
storage: localStorage, // any Storage object, sessionStorage (default) or localStorage
}

const sdk = new SDK(sdkConfig)
```

## More examples

To see how to use casdoor frontend SDK with casdoor backend SDK, you can refer to examples below:
Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@
"@semantic-release/npm": "^7.1.3",
"@semantic-release/release-notes-generator": "^9.0.3",
"@types/jest": "^27.0.2",
"jest": "^27.2.1",
"npm-run-all": "^4.1.5",
"typescript": "^4.5.5",
"rimraf": "^3.0.2",
"jest": "^27.2.1",
"semantic-release": "19.0.3",
"ts-jest": "^27.0.5",
"semantic-release": "^17.4.4"
"typescript": "^4.5.5"
},
"files": [
"lib"
Expand All @@ -74,5 +74,8 @@
"bugs": {
"url": "https://github.com/casdoor/casdoor-js-sdk/issues"
},
"homepage": "https://github.com/casdoor/casdoor-js-sdk"
"homepage": "https://github.com/casdoor/casdoor-js-sdk",
"dependencies": {
"js-pkce": "^1.3.0"
}
}
60 changes: 49 additions & 11 deletions src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import PKCE from 'js-pkce';
import ITokenResponse from "js-pkce/dist/ITokenResponse";
import IObject from "js-pkce/dist/IObject";

export interface SdkConfig {
serverUrl: string, // your Casdoor server URL, e.g., "https://door.casbin.com" for the official demo site
clientId: string, // the Client ID of your Casdoor application, e.g., "014ae4bd048734ca2dea"
appName: string, // the name of your Casdoor application, e.g., "app-casnode"
organizationName: string // the name of the Casdoor organization connected with your Casdoor application, e.g., "casbin"
redirectPath?: string // the path of the redirect URL for your Casdoor application, will be "/callback" if not provided
signinPath?: string // the path of the signin URL for your Casdoor applcation, will be "/api/signin" if not provided
scope?: string // apply for permission to obtain the user information, will be "profile" if not provided
storage?: Storage // the storage to store the state, will be sessionStorage if not provided
}

// reference: https://github.com/casdoor/casdoor-go-sdk/blob/90fcd5646ec63d733472c5e7ce526f3447f99f1f/auth/jwt.go#L19-L32
Expand All @@ -40,21 +46,26 @@ export interface Account {

class Sdk {
private config: SdkConfig
private pkce : PKCE

constructor(config: SdkConfig) {
this.config = config
if (config.redirectPath === undefined || config.redirectPath === null) {
this.config.redirectPath = "/callback";
}
}

public getSignupUrl(enablePassword: boolean = true): string {
if (enablePassword) {
sessionStorage.setItem("signinUrl", this.getSigninUrl());
return `${this.config.serverUrl.trim()}/signup/${this.config.appName}`;
} else {
return this.getSigninUrl().replace("/login/oauth/authorize", "/signup/oauth/authorize");
if(config.scope === undefined || config.scope === null) {
this.config.scope = "profile";
}

this.pkce = new PKCE({
client_id: this.config.clientId,
redirect_uri: `${window.location.origin}${this.config.redirectPath}`,
authorization_endpoint: `${this.config.serverUrl.trim()}/login/oauth/authorize`,
token_endpoint: `${this.config.serverUrl.trim()}/api/login/oauth/access_token`,
requested_scopes: this.config.scope || "profile",
storage: this.config.storage,
});
}

getOrSaveState(): string {
Expand All @@ -72,11 +83,19 @@ class Sdk {
sessionStorage.removeItem("casdoor-state");
}

public getSignupUrl(enablePassword: boolean = true): string {
if (enablePassword) {
sessionStorage.setItem("signinUrl", this.getSigninUrl());
return `${this.config.serverUrl.trim()}/signup/${this.config.appName}`;
} else {
return this.getSigninUrl().replace("/login/oauth/authorize", "/signup/oauth/authorize");
}
}

public getSigninUrl(): string {
const redirectUri = this.config.redirectPath && this.config.redirectPath.includes('://') ? this.config.redirectPath : `${window.location.origin}${this.config.redirectPath}`;
const scope = "read";
const state = this.getOrSaveState();
return `${this.config.serverUrl.trim()}/login/oauth/authorize?client_id=${this.config.clientId}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&state=${state}`;
return `${this.config.serverUrl.trim()}/login/oauth/authorize?client_id=${this.config.clientId}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${this.config.scope}&state=${state}`;
}

public getUserProfileUrl(userName: string, account: Account): string {
Expand Down Expand Up @@ -135,12 +154,12 @@ class Sdk {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = `${this.getSigninUrl()}&silentSignin=1`;

const handleMessage = (event: MessageEvent) => {
if (window !== window.parent) {
return null;
}

const message = event.data;
if (message.tag !== "Casdoor" || message.type !== "SilentSignin") {
return;
Expand Down Expand Up @@ -183,6 +202,25 @@ class Sdk {

window.addEventListener("message", handleMessage);
}

public async signin_redirect(additionalParams?: IObject): Promise<void> {
window.location.replace(this.pkce.authorizeUrl(additionalParams));
}

public async exchangeForAccessToken(additionalParams?: IObject): Promise<ITokenResponse> {
return this.pkce.exchangeForAccessToken(window.location.href, additionalParams);
}

public async getUserInfo(accessToken: string): Promise<Response> {
return fetch(`${this.config.serverUrl.trim()}/api/userinfo`, {
method: "GET",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
}).then(res => res.json()
);
}
}

export default Sdk;
Loading

0 comments on commit e25d27d

Please sign in to comment.