Skip to content
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
50 changes: 21 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

[![Code Owners](https://img.shields.io/badge/owner-platform-blueviolet?style=flat&logo=github)](./.github/CODEOWNERS)

This repo contains both examples for shine regulated partners api and shine public api
## Purpose
This repo contains both examples for Regulated partners and the public API

This projects aims at demonstrating how to use Shine Connect API, including the mTLS setup.

Expand All @@ -12,49 +13,40 @@ See the full Shine Connect documentation [here](https://developers.shine.fr/v3.1

## Install

```
```shell
yarn install
```

## Configuration
## General configuration

Copy `server/config/config.example.json` to a new `server/config/config.json` and fill the following values

| Variable | Description |
| -------------- | ---------------------------------------------------------------------------------------------------- |
| CLIENT_ID | Client ID given at the creation |
| CLIENT_SECRET | Secret given at the creation |
| SCOPE | Scope to be granted, will be presented to the user |
| REDIRECT_URI | Redirect URI once authorization is granted. Make sure it is whitelisted in the client `redirectURIs` |
| WEBHOOK_SECRET | Secret provided by shine to check webhook signature |
| Variable | Description |
|-----------------|------------------------------------------------------------------------------------------------------|
| PSD2_REGULATION | Whether you are subject to PSD2 regulation |
| CLIENT_ID | Client ID given at the creation |
| CLIENT_SECRET | Secret given at the creation |
| SCOPE | Scope to be granted, will be presented to the user |
| REDIRECT_URI | Redirect URI once authorization is granted. Make sure it is whitelisted in the client `redirectURIs` |
| WEBHOOK_SECRET | Secret provided by shine to check webhook signature (optional) |

# Shine connect for regulated partners (DSP2)
# Shine Connect for Regulated partners (DSP2)

### Configuration QSEAL and QWAC for DSP2

Add the necessary certificates for mTLS connection:

- server/certificates/QSEAL_KEY.pem, it should contain your QSEAL key
- server/certificates/QWAC_KEY.pem, it should contain your QWAC key
- server/certificates/QWAC_CERT.pem, it should contain your QWAC certificate
- server/certificates/ROOT_CA.pem, it should contain the certificate chain of the root certificate(s) necessary to use you QWAC certificate
- `server/certificates/QSEAL_KEY.pem`, it should contain your QSEAL key
- `server/certificates/QWAC_KEY.pem`, it should contain your QWAC key
- `server/certificates/QWAC_CERT.pem`, it should contain your QWAC certificate
- `server/certificates/ROOT_CA.pem`, it should contain the certificate chain of the root certificate(s) necessary to use your QWAC certificate

## Run
## Run

```
```shell
yarn dev
```

# Shine connect public api

- You need to export PUBLIC_API

```
export PUBLIC_API=true
```

- then we can run it
### On your local environment

```
yarn dev
```
Open your browser and go to [http://localhost:9876/](http://localhost:9876/).
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
"validate-signature": "tsc scripts/*.ts && node scripts/validate-signature.js"
},
"dependencies": {
"axios": "^1.6.7",
"express": "^4.18.2",
"axios": "^1.20.0",
"express": "^5.2.1",
"http-signature": "^1.4.0",
"next": "^14.1.0",
"qs": "^6.11.2",
"next": "^16.3.3",
"qs": "^6.15.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-pro-sidebar": "^1.1.0",
Expand Down
8 changes: 6 additions & 2 deletions pages/containers/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { AuthenticatedData } from '../utils';
// parse authorized, uid, access_token and refresh_token from the query string
const parseQueryString = (router: NextRouter) => qs.parse(router.asPath.split('?')[1]);

function App() {
function App({ isPublicAPI }: { isPublicAPI: boolean }) {
const router = useRouter();
const [authenticatedData, setAuthenticatedData] = useState<AuthenticatedData>(undefined);

Expand All @@ -19,7 +19,11 @@ function App() {
setAuthenticatedData(params);
}, [router, setAuthenticatedData]);

return authenticatedData?.authorized ? <Authenticated authenticatedData={authenticatedData} /> : <SignIn />;
return authenticatedData?.authorized ? (
<Authenticated authenticatedData={authenticatedData} isPublicAPI={isPublicAPI} />
) : (
<SignIn isPublicAPI={isPublicAPI} />
);
}

export default App;
10 changes: 8 additions & 2 deletions pages/containers/Authenticated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,18 @@ const StyledSidebarHeader = styled.div`
}
`;

function Authenticated({ authenticatedData }: { authenticatedData: AuthenticatedData }) {
function Authenticated({
authenticatedData,
isPublicAPI,
}: {
authenticatedData: AuthenticatedData;
isPublicAPI: boolean;
}) {
const [operationOutput, setOperationOutput] = useState<string>(null);
const [error, setError] = useState<string>(null);

if (!authenticatedData.authorized) {
return <SignIn />;
return <SignIn isPublicAPI={isPublicAPI} />;
}

return (
Expand Down
22 changes: 7 additions & 15 deletions pages/containers/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Head from 'next/head';
import { useState } from 'react';
import Button from '../components/Button';
import ScopeList from '../components/ScopeList';
import { dsp2Scopes, minimalScopes, publicApiScopes } from '../../server/config/scopes';
import { isPublicAPI } from '../../server/config';

const Container = styled.div`
display: flex;
Expand All @@ -19,21 +21,11 @@ const H2 = styled.h2`
margin-bottom: 20px;
`;

function SignIn() {
function SignIn({isPublicAPI}: {isPublicAPI: boolean}) {
const router = useRouter();
const [selectedValues, setSelectedValues] = useState(minimalScopes);

const values = [
'openid',
'profile',
'user',
'company:profile:read',
'email',
'bank',
'phone',
'invoices:read',
'receipts:read',
];
const [selectedValues, setSelectedValues] = useState(['openid', 'profile', 'user:profile:read']);
const scopes = isPublicAPI ? publicApiScopes : dsp2Scopes;

const handleSelectedValuesChange = (newSelectedValues: string[]) => {
setSelectedValues(newSelectedValues);
Expand All @@ -48,8 +40,8 @@ function SignIn() {
<Head>
<title>Shine Connect</title>
</Head>
<H2>Select scope that you want to access:</H2>
<ScopeList values={values} selectedValues={selectedValues} onSelectedValuesChange={handleSelectedValuesChange} />
<H2>Select scope that you want to access</H2>
<ScopeList values={scopes} selectedValues={selectedValues} onSelectedValuesChange={handleSelectedValuesChange} />
<Button text="Login with Shine" onClick={handleLogin} />
</Container>
);
Expand Down
10 changes: 8 additions & 2 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { GetServerSideProps } from 'next';
import App from './containers/App';
import { isPublicAPI } from '../server/config';

export default function Home() {
return <App />;
export default function Home({ isPublicAPI }: { isPublicAPI: boolean }) {
return <App isPublicAPI={isPublicAPI} />;
}

export const getServerSideProps: GetServerSideProps = async () => {
return { props: { isPublicAPI } };
};
17 changes: 11 additions & 6 deletions server/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,32 @@ import qs from 'qs';
import axios from 'axios';

const callback = async (req: Request, res: Response) => {
const { code, error } = req.query;
if (error || !code) {
console.log('Authorization request denied 😞');
const { code: authorizationCode, error } = req.query;

if (error && !authorizationCode) {
console.warn('Error occurred; authorization denied ⛔️', error);
return res.redirect('/?authorized=false');
}

console.log('Authorization request accepted 🎉');

try {
// ask for consent
const response = await axios.get(`${shineAuthHost}/oauth2/token`, {
params: {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'authorization_code',
code,
code: authorizationCode,
redirect_uri: redirectUri,
},
});

const { access_token, refresh_token, metadata } = response.data;
console.log('Tokens retrieved ✅');

const { companyProfileId, uid, companyUserId } = metadata;

// Display success
// DANGER: This an example, in a real world better to not share the access_token with the client application.
res.redirect(
Expand All @@ -37,8 +42,8 @@ const callback = async (req: Request, res: Response) => {
uid,
})}`,
);
} catch (e) {
console.error(e);
} catch (error) {
console.error('Error retrieving tokens ⛔️', error);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💅 I'm not a huge fan of logs containing emojis if they end up in an actual logging system (#Datadog) 🫣

I can see the previous logs with some, but just sayin' 😁

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this project purpose is to help partner to integrate shine connect. this is an example of how to implement the api calls and the result of some endpoints

res.redirect('/?authorized=false');
}
};
Expand Down
9 changes: 5 additions & 4 deletions server/config/config.example.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"CLIENT_ID": "",
"CLIENT_SECRET": "",
"SCOPE": "",
"REDIRECT_URI": "",
"PSD2_REGULATION": false,
"CLIENT_ID": "__your_client_id__",
"CLIENT_SECRET": "__your_client_secret__",
"SCOPE": "__your_scopes__",
"REDIRECT_URI": "https://__your_redirection_callback__",
"WEBHOOK_SECRET": "",
"KEY_ID": "",
"QWAC_KEY_PATH": "./server/certificates/QWAC_KEY.pem",
Expand Down
2 changes: 1 addition & 1 deletion server/config/getHosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const SHINE_REGULATED_DEV_HOST = 'localhost';

const SHINE_PUBLIC_PRODUCTION_HOST = 'https://public.api.shine.fr';
const SHINE_PUBLIC_STAGING_HOST = 'https://public.api.staging.shine.fr';
const SHINE_PUBLIC_DEV_HOST = 'http://localhost:10081';
const SHINE_PUBLIC_DEV_HOST = 'http://localhost:10164'; // using dev-proxy to forward requests to the public API in dev mode

/**
*
Expand Down
14 changes: 10 additions & 4 deletions server/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { getHosts } from './getHosts';
import config from './config.json';
import { dsp2Scopes, publicApiScopes } from './scopes';

const {
PSD2_REGULATION: psd2Regulation,
CLIENT_ID: clientId,
CLIENT_SECRET: clientSecret,
SCOPE: defaultScope,
SCOPE: clientScopes,
REDIRECT_URI: redirectUri,
WEBHOOK_SECRET: webhookSecret,
KEY_ID: keyId,
Expand All @@ -17,28 +20,31 @@ const isStaging = process.env.API_ENV === 'staging';
const isProd = process.env.API_ENV === 'production';
const isLocal = !isProd && !isStaging;

const isPublicAPI = process.env.PUBLIC_API === 'true';
const isPublicAPI = psd2Regulation === false;

const port = process.env.PORT || 9876;
const dev = isLocal || isStaging;

const { shineApiHost, shineAuthHost } = getHosts(isLocal, isStaging, isPublicAPI);

const availableScopes = isPublicAPI ? publicApiScopes : dsp2Scopes;

export {
shineAuthHost,
shineApiHost,
redirectUri,
port,
clientId,
clientSecret,
defaultScope,
clientScopes,
availableScopes,
dev,
isLocal,
isPublicAPI,
webhookSecret,
keyId,
qwacKeyPath,
qwacCertPath,
qsealKeyPath,
rootCAPath,
isPublicAPI,
};
27 changes: 27 additions & 0 deletions server/config/scopes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const minimalScopes = ['openid', 'profile', 'user:profile:read'];

export const dsp2Scopes = [
'openid',
'profile',
'user',
'company:profile:read',
'email',
'bank',
'phone',
'invoices:read',
'receipts:read',
];
export const publicApiScopes = [
'openid',
'profile',
'email',
'phone',
'user:profile:read',
'company:profile:read',
'bank:transactions:read',
'bank:transfers:recipients:read',
'bank:transfers:read',
'bank:accounts:read',
'receipts:read',
'invoices:read',
];
6 changes: 4 additions & 2 deletions server/login.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import qs from 'qs';
import { Request, Response } from 'express';
import { redirectUri, clientId, defaultScope, port, shineAuthHost } from './config';
import { clientId, clientScopes, redirectUri, shineAuthHost } from './config';

const login = async (req: Request, res: Response) => {
const { requestedScope } = req.query;
const scope = requestedScope ?? defaultScope;
const scope = requestedScope ?? clientScopes;

const redirectTo = `${shineAuthHost}/oauth2/authorize?${qs.stringify({
client_id: clientId,
scope,
redirect_uri: redirectUri,
})}`;

res.redirect(redirectTo);
};

Expand Down
10 changes: 6 additions & 4 deletions server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ app.prepare().then(() => {
const server = express();
// This is required to parse the request body as raw to check the signature
server.use(express.json({ verify: rawBodySaver }));
console.log('Loaded configuration', config);

const redirectUriPath = url.parse(redirectUri).pathname;
console.log('Loaded configuration: ', config);

server.get('/login', login);
server.get(redirectUriPath, callback);
server.get('/refresh-token', refreshToken);

const redirectUriPath = url.parse(redirectUri).pathname;
server.get(redirectUriPath, callback);

server.get('/user-profile', getUserProfile);
server.get('/company-profile', getCompanyProfile);
server.get('/bank-accounts', getBankAccounts);
Expand All @@ -69,6 +70,7 @@ app.prepare().then(() => {
server.post('/webhook-handler', webhookHandler);
server.get('/webhook', webhook);

server.get('*', (req, res) => handle(req, res));
server.get('/{*splat}', (req, res) => handle(req, res));

server.listen(port, () => console.info(`Server listening on port ${port}`));
});
Loading