Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sdk for web front-only app #47

Merged
merged 2 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@

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 @@
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");

Check warning on line 91 in src/sdk.ts

View check run for this annotation

Codecov / codecov/patch

src/sdk.ts#L88-L91

Added lines #L88 - L91 were not covered by tests
}
}

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 @@
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 @@

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

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

Check warning on line 207 in src/sdk.ts

View check run for this annotation

Codecov / codecov/patch

src/sdk.ts#L206-L207

Added lines #L206 - L207 were not covered by tests
}

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

Check warning on line 211 in src/sdk.ts

View check run for this annotation

Codecov / codecov/patch

src/sdk.ts#L210-L211

Added lines #L210 - L211 were not covered by tests
}

public async getUserInfo(accessToken: string): Promise<Response> {
return fetch(`${this.config.serverUrl.trim()}/api/userinfo`, {

Check warning on line 215 in src/sdk.ts

View check run for this annotation

Codecov / codecov/patch

src/sdk.ts#L214-L215

Added lines #L214 - L215 were not covered by tests
method: "GET",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
}).then(res => res.json()

Check warning on line 221 in src/sdk.ts

View check run for this annotation

Codecov / codecov/patch

src/sdk.ts#L221

Added line #L221 was not covered by tests
);
}
}

export default Sdk;
Loading
Loading