Skip to content

Commit e849406

Browse files
authored
Merge pull request #4 from descope-sample-apps/sso
SSO via OIDC (Tenant)
2 parents 118b09a + a2c40fe commit e849406

File tree

9 files changed

+218
-11
lines changed

9 files changed

+218
-11
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"java.configuration.updateBuildConfiguration": "interactive"
3+
}

README.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Java React Sample App
1+
# Java React Sample App
22

3-
This sample app showcases Descope authentication built using React for frontend and Java Spring for backend. The frontend incldues a home, login, and dashboard screen, with the dashboard including a call to the backend to get a "secret message" that is only shared when a valid session token is passed in.
3+
This sample app showcases Descope authentication built using React for frontend and Java Spring for backend. The frontend incldues a home, login, and dashboard screen, with the dashboard including a call to the backend to get a "secret message" that is only shared when a valid session token is passed in.
44

55
Authentication and session validation are implemented using Descope's [React SDK](https://github.com/descope/react-sdk) and [Java SDK](https://github.com/descope/descope-java) in the frontend and backend respectively.
66

@@ -9,11 +9,13 @@ Authentication and session validation are implemented using Descope's [React SDK
99
### Run server
1010

1111
1. Navigate into the server folder:
12+
1213
```
1314
cd server
1415
```
1516

1617
2. In your `application.properties` file, add your Descope project ID:
18+
1719
```
1820
descope.project.id=<YOUR_DESCOPE_PROJECT_ID>
1921
```
@@ -30,34 +32,65 @@ If you use Maven, run the following command in a terminal window (in the complet
3032
./mvnw spring-boot:run
3133
```
3234

33-
3435
### Run client
3536

3637
1. Navigate into the client folder:
38+
3739
```
3840
cd client
3941
```
4042

4143
2. Install dependencies
44+
4245
```
4346
npm i
4447
```
4548

4649
3. Create a `.env` folder and add environment variables:
50+
4751
```
4852
REACT_APP_DESCOPE_PROJECT_ID="YOUR_DESCOPE_PROJECT_ID"
4953
```
5054

5155
4. Start the application
56+
5257
```
5358
npm start
5459
```
5560

56-
>Note: If you're not running the client at http://localhost:3000 you may need to change the server's CrossOrigin domain to wherever you're hosting it (in JavaSampleAppApplication.java).
61+
>Note: If you're not running the client at <http://localhost:3000> you may need to change the server's CrossOrigin domain to wherever you're hosting it (in JavaSampleAppApplication.java).
62+
5763
```
5864
@CrossOrigin(origins = "http://localhost:3000")
5965
```
6066

67+
## Tenant-based OIDC SSO Setup
68+
69+
You will need to configure a tenant in your Descope console with OIDC. Then, you can use the associated tenant ID to start SSO, redirect to the IdP authentication portal, and then exchange the returned code for
70+
authenticated user info. We'll include the steps to set up in the UI here, but this can also be done via API or SDK.
71+
72+
1. Create a tenant [here](https://app.descope.com/tenants)
73+
2. Then, click on the tenant, Authentication Methods, SSO and enable and configure SSO via OIDC with an Identity Provider
74+
75+
Be sure to have `https://api.descope.com/v1/oauth/callback` in the allowed redirect URIs
76+
77+
![Screenshot 2024-02-17 at 10 42 35 AM](https://github.com/descope-sample-apps/java-react-sample-app/assets/46854522/76cf59da-5e8e-4067-b601-23445b05bf77)
78+
79+
3. Run your application per `Setup & Running` as described above, with the client at http://localhost:3000 and server at http://localhost:8080.
80+
81+
4. Navigate to the url where your client is running and input the tenant ID.
82+
83+
![Screenshot 2024-02-17 at 10 38 23 AM](https://github.com/descope-sample-apps/java-react-sample-app/assets/46854522/a7647954-c166-447a-b848-0171364210a2)
84+
85+
5. Log in via your IdP. Then, you'll be redirected back to the application where the SSO exchange will complete.
86+
87+
![Screenshot 2024-02-17 at 10 46 38 AM](https://github.com/descope-sample-apps/java-react-sample-app/assets/46854522/0159a703-20a3-4a2e-b25c-120c043f66a2)
88+
89+
You should see the signed in user's email, userId, session, and refresh token.
90+
![Screenshot 2024-02-18 at 10 22 19 AM](https://github.com/descope-sample-apps/java-react-sample-app/assets/46854522/3feb9585-d508-4d8b-ac13-e1cd54307c3f)
91+
92+
93+
6194
## License
6295

6396
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useEffect, useRef, useState } from "react";
2+
import { useNavigate } from "react-router-dom";
3+
4+
const AuthorizationCodeCallback = () => {
5+
const [response, setResponse] = useState(null);
6+
const [error, setError] = useState("");
7+
const hasFetchedRef = useRef(false);
8+
9+
const navigate = useNavigate();
10+
11+
useEffect(() => {
12+
const urlParams = new URLSearchParams(window.location.search);
13+
const code = urlParams.get('code');
14+
15+
// Ref added to prevent calling of endpoint twice,
16+
// due to React Strict Mode behavior in development
17+
// for debugging purposes
18+
if (hasFetchedRef.current) {
19+
return;
20+
}
21+
hasFetchedRef.current = true;
22+
23+
if (code) {
24+
25+
fetch(`http://localhost:8080/authorization-code/callback`, {
26+
method: 'POST',
27+
headers: {
28+
'Content-Type': 'application/json',
29+
'Accept': 'application/json',
30+
},
31+
body: JSON.stringify({ code }),
32+
})
33+
.then(async response => {
34+
const data = await response.json();
35+
setResponse(data);
36+
// Navigate to protected page
37+
// navigate('/protected', { replace: true });
38+
})
39+
.catch(error => {
40+
console.error('Error:', error);
41+
setError(error);
42+
});
43+
}
44+
}, []);
45+
46+
47+
48+
if (error) {
49+
return ( <p>{error.toString()}</p>);
50+
}
51+
52+
if (response) {
53+
return (
54+
<div>
55+
<p>{response.email}</p>
56+
<p>{response.userId}</p>
57+
<p>{response.token}</p>
58+
<p>{response.refreshToken}</p>
59+
</div>
60+
);
61+
}
62+
63+
return (
64+
<div>Loading...</div>
65+
);
66+
}
67+
68+
export default AuthorizationCodeCallback;

client/src/components/SSOSignIn.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useState } from "react";
2+
3+
const SSOSignIn = () => {
4+
5+
const [tenantId, setTenantId] = useState("");
6+
const startSSOSignIn = () => {
7+
if (!tenantId) {
8+
alert("Please enter a tenant ID");
9+
return;
10+
} else {
11+
const queryParams = new URLSearchParams({
12+
tenantId,
13+
redirectUrl: 'http://localhost:3000/authorization-code/callback',
14+
}).toString();
15+
fetch(`http://localhost:8080/start_sso?${queryParams}`, {
16+
headers: {
17+
Accept: 'application/json',
18+
}
19+
}).then(async (response) => {
20+
const url = (await response.json()).url;
21+
console.log(url)
22+
window.location.href = url;
23+
}).catch((error) => {
24+
console.log(error)
25+
alert("Error starting SSO sign in.")
26+
});
27+
}
28+
}
29+
30+
return <>
31+
<h1>Tenant SSO OIDC Sign In</h1>
32+
<input type="text" value={tenantId} placeholder="Tenant ID" onChange={(e) => setTenantId(e.target.value)} />
33+
<button onClick={startSSOSignIn}>Sign in via SSO</button>
34+
</>
35+
}
36+
37+
export default SSOSignIn;

client/src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom/client';
3-
import App from './App';
43
import { AuthProvider } from '@descope/react-sdk';
54
import { BrowserRouter, Routes, Route } from "react-router-dom";
65
import Layout from "./pages/Layout";
76
import Home from "./pages/Home";
87
import Dashboard from "./pages/Dashboard";
98
import SignIn from "./pages/SignIn";
9+
import AuthorizationCodeCallback from './components/AuthorizationCodeCallback';
1010

1111
const root = ReactDOM.createRoot(document.getElementById('root'));
1212
root.render(
@@ -18,6 +18,7 @@ root.render(
1818
<Routes>
1919
<Route path="/" element={<Layout />}>
2020
<Route index element={<Home />} />
21+
<Route path="/authorization-code/callback" element={<AuthorizationCodeCallback />} />
2122
<Route path="/dashboard" element={<Dashboard />} />
2223
<Route path="/signin" element={<SignIn />} />
2324
</Route>

client/src/pages/Home.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useDescope, useSession, useUser } from '@descope/react-sdk'
44
import { Descope } from '@descope/react-sdk'
55
import { getSessionToken } from '@descope/react-sdk';
66
import SecretMessage from '../components/SecretMessage';
7+
import SSOSignIn from '../components/SSOSignIn';
78

89

910
const Home = () => {
@@ -25,6 +26,8 @@ const Home = () => {
2526
<h1>Welcome to your home page</h1>
2627
<button onClick={() => window.location.href = '/signin'}>Sign In</button>
2728
<SecretMessage/>
29+
<SSOSignIn/>
30+
2831
</div>
2932
</>;
3033
}

server/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
<dependency>
3232
<artifactId>java-sdk</artifactId>
3333
<groupId>com.descope</groupId>
34-
<version>1.0</version>
34+
<version>1.0.14</version>
3535
</dependency>
3636

3737
</dependencies>

server/src/main/java/com/descope/java_sample_app/JavaSampleAppApplication.java

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,51 @@
11
package com.descope.java_sample_app;
2+
23
import com.descope.client.*;
34
import com.descope.exception.DescopeException;
5+
import com.descope.model.auth.AuthenticationInfo;
46
import com.descope.model.jwt.Token;
7+
import com.descope.model.magiclink.LoginOptions;
58
import com.descope.sdk.auth.*;
9+
10+
import jakarta.annotation.PostConstruct;
611
import jakarta.servlet.http.HttpServletRequest;
12+
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
716
import org.springframework.beans.factory.annotation.Value;
817
import org.springframework.boot.SpringApplication;
918
import org.springframework.boot.autoconfigure.SpringBootApplication;
1019
import org.springframework.http.HttpStatus;
1120
import org.springframework.http.ResponseEntity;
1221
import org.springframework.web.bind.annotation.CrossOrigin;
1322
import org.springframework.web.bind.annotation.GetMapping;
23+
import org.springframework.web.bind.annotation.PostMapping;
24+
import org.springframework.web.bind.annotation.RequestBody;
25+
import org.springframework.web.bind.annotation.RequestParam;
1426
import org.springframework.web.bind.annotation.RestController;
1527

1628
@SpringBootApplication
1729
@RestController
1830
@CrossOrigin(origins = "http://localhost:3000")
1931
public class JavaSampleAppApplication {
2032

33+
34+
@Value("${descope.project.id}")
35+
private String descopeProjectId;
36+
37+
private DescopeClient descopeClient;
38+
2139
public static void main(String[] args) {
2240
SpringApplication.run(JavaSampleAppApplication.class, args);
2341
}
24-
@Value("${descope.project.id}")
25-
private String descopeProjectId;
42+
43+
@PostConstruct
44+
public void init() {
45+
descopeClient = new DescopeClient(Config.builder().projectId(descopeProjectId).build());
46+
}
2647

2748
public void validateSession(String sessionToken) throws DescopeException {
28-
var descopeClient = new DescopeClient(Config.builder().projectId(descopeProjectId).build());
2949
AuthenticationService as = descopeClient.getAuthenticationServices().getAuthService();
3050
Token t = as.validateSessionWithToken(sessionToken);
3151
}
@@ -60,4 +80,46 @@ public ResponseEntity<String> getSecretMessage(HttpServletRequest request) {
6080
return new ResponseEntity<>("Error getting authorization header", HttpStatus.UNAUTHORIZED);
6181
}
6282
}
63-
}
83+
84+
@GetMapping("/start_sso")
85+
public ResponseEntity<String> startSSOEndpoint(
86+
@RequestParam("tenantId") String tenantId,
87+
@RequestParam(value = "redirectUrl", required = false) String redirectUrl,
88+
@RequestParam(value = "prompt", required = false) String prompt,
89+
@RequestParam(value = "loginOptions", required = false) LoginOptions loginOptions) {
90+
try {
91+
String url = descopeClient.getAuthenticationServices().getSsoServiceProvider().start(tenantId, redirectUrl, prompt,
92+
loginOptions);
93+
String jsonResponse = "{\"url\": \"" + url + "\"}";
94+
95+
return ResponseEntity.ok()
96+
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
97+
.body(jsonResponse);
98+
} catch (DescopeException e) {
99+
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
100+
}
101+
}
102+
103+
@PostMapping("/authorization-code/callback")
104+
public ResponseEntity<?> handleAuthorizationCode(@RequestBody Map<String, String> payload) {
105+
try {
106+
String code = payload.get("code");
107+
AuthenticationInfo authInfo = descopeClient.getAuthenticationServices().getSsoServiceProvider().exchangeToken(code);
108+
String email = authInfo.getUser().getEmail();
109+
String userId = authInfo.getUser().getUserId();
110+
String token = authInfo.getToken().toString();
111+
String refreshToken = authInfo.getRefreshToken().toString();
112+
113+
Map<String, String> response = new HashMap<>();
114+
response.put("email", email);
115+
response.put("userId", userId);
116+
response.put("token", token);
117+
response.put("refreshToken", refreshToken);
118+
System.out.println("Response: " + response.toString());
119+
return ResponseEntity.ok(response);
120+
} catch (DescopeException e) {
121+
System.out.println("Error: " + e.getMessage());
122+
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
123+
}
124+
}
125+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
descope.project.id=<your_project_id>
1+
descope.project.id=<your descope project id>

0 commit comments

Comments
 (0)