Skip to content

Commit 845b5b9

Browse files
authored
Merge pull request #5 from UseKeyp/dev
Initial release
2 parents fcadcf4 + 3e47c01 commit 845b5b9

File tree

12 files changed

+135
-76
lines changed

12 files changed

+135
-76
lines changed

README.md

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,94 @@
55
</a>
66
</p>
77

8-
> An OAuth2 provider built using Redwood and oidc-provider
9-
10-
<p align="left">
11-
<img width="600px" src="oauth-server-redwood-demo.gif"/>
12-
</p>
8+
> OAuth2 server with dynamic client registration, built with Oidc-Provider for RedwoodJS
139
1410
🚧 IN DEVELOPMENT 🚧
1511

16-
"Authority" means that you are providing authentication or authorization as a service for _other apps_. For example "Sign in with MyCompanyApp", as opposed to "Sign in with Google". If you're just looking to implement an OAuth2 client in your app, check out [`oauth2-client-redwood`](https://github.com/usekeyp/oauth2-client-redwood).
12+
"Authority" means that you are providing authentication or authorization as a service for _other apps_. For example "Sign in with MyCompanyApp", as opposed to "Sign in with Google". If you're just looking to implement an OAuth2 client in your app, check out [`oauth2-client-redwood`][oauth2-client-redwood].
1713

1814
## Demo ⏯️
1915

2016
Hosted demo coming soon
2117

2218
In the example gif above, its important to note that the server is wrapping the user's Discord account with its own account (double authentication). The flow could also just use normal username/password.
23-
## Developing
19+
## Usage
20+
21+
1. Create a new function `oauth` and install the package
22+
23+
```bash
24+
yarn add oauth2-server-redwood serverless-http
25+
```
26+
27+
```js
28+
// api/src/functions/oauth.js
29+
import oauth2Server from 'oauth2-server-redwood'
30+
import serverless from 'serverless-http'
31+
32+
import { db } from 'src/lib/db'
33+
34+
export const handler = serverless(
35+
oauth2Server(db, {
36+
SECURE_KEY: process.env.SECURE_KEY,
37+
APP_DOMAIN: process.env.APP_DOMAIN,
38+
routes: { login: '/login', authorize: '/authorize' },
39+
config: {
40+
// Define your own OIDC-Provider config (https://github.com/panva/node-oidc-provider)
41+
clients: [
42+
{
43+
client_id: '123',
44+
client_secret: 'somesecret',
45+
redirect_uris: [
46+
'https://jwt.io',
47+
'https://oauthdebugger.com/debug',
48+
'http://0.0.0.0:8910/redirect/oauth2_server_redwood',
49+
],
50+
},
51+
],
52+
},
53+
})
54+
)
55+
```
56+
57+
2. Copy the .env.example to .env and update the values
58+
59+
3. Setup an Nginx proxy. I've included `oauth2-server-redwood.conf` which removes the prefix and serves the endpoint from `localhost/oauth` instead of `localhost/api/oauth`. Oidc-provider does not always adhere to the `/api` path prefix when setting cookie path, or my implementation is incorrect. If you you can help solve this, please let me know!
2460

25-
Here's the user-agent flow for a standard node-oidc-provider. Note ours is slightly modified, since we use our Redwood app UI for the login and consent screens.
61+
4. Setup dbAuth and update the graphql schema. Copy the schema here or see [`oauth2-client-redwood`][oauth2-client-redwood].
2662

27-
<img src="user-agent-flow.png"/>
63+
```bash
64+
yarn rw setup auth dbAuth
65+
```
66+
67+
5. To test, you can use https://oauthdebugger.com/
68+
69+
- Authorize URI: http://localhost/oauth/auth
70+
- Client ID: 123
71+
- Scope: openid email profile
72+
- Use PKCE: true
73+
74+
<img width="400px" src="oauth-debugger.png">
75+
76+
Alternatively, you can test using only Redwood apps. Clone [`oauth2-client-redwood`][oauth2-client-redwood] and update the line in the `.env` file to point to your server:
77+
78+
```
79+
OAUTH2_SERVER_REDWOOD_API_DOMAIN=http://localhost/oauth
80+
```
2881

2982
## Contributing 💡
3083

3184
To run this repo locally:
3285

33-
- Update your .env from `.env.example`.
34-
- You'll need to setup a nginx proxy, since oidc-provider sometimes ignores the extra `/api` path prefix, and cookie paths are not set properly. I've included `oauth2-server-redwood.conf` which removes the prefix and serves the endpoint from `localhost/oauth` instead of `localhost/api/oauth`. I'm open to other ideas here if you'd like to help!
35-
- Run `yarn build` in `/packages/oauth2-server
36-
86+
- Clone the repo and follow steps 2 & 3 above
87+
- Run `yarn build:watch` in `/packages/oauth2-server`
88+
- Run `yarn rw dev` to start the app
3789
## TODO
3890

3991
- [x] Validate rw session tokens during login
40-
- [ ] Add claims to the user model and fetch in `findAccount`
92+
- [ ] Upgrade to latest oidc-provider (blocked by lack of support for "require")
93+
- [ ] Add dbAuth username/password option to make the demo simpler to understand
4194
- [ ] Show proper scopes for consent page
4295
- [ ] Improve the UI
43-
- [ ] Fix redirect bug to /profile
44-
- [ ] Add dbAuth username/password option to make the demo simpler to understand
45-
- [ ] Security audit
4696
## Resources 🧑‍💻
4797

4898
- OAuth Server libraries: https://oauth.net/code/nodejs/
@@ -58,6 +108,5 @@ Copyright © 2023 Nifty Chess, Inc.<br />
58108
This project is MIT licensed.
59109

60110
[sponsor-keyp]: https://UseKeyp.com
61-
62-
111+
[oauth2-client-redwood]: https://github.com/UseKeyp/oauth2-client-redwood
63112

api/src/functions/oauth/oauth.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
1-
import app from 'oauth2-server-redwood'
1+
import oauth2Server from 'oauth2-server-redwood'
22
import serverless from 'serverless-http'
33

44
import { db } from 'src/lib/db'
55

6-
export const handler = serverless(app({ db }))
6+
export const handler = serverless(
7+
oauth2Server(db, {
8+
SECURE_KEY: process.env.SECURE_KEY,
9+
APP_DOMAIN: process.env.APP_DOMAIN,
10+
routes: { login: '/login', authorize: '/authorize' },
11+
config: {
12+
// OIDC-Provider config, see https://github.com/panva/node-oidc-provider
13+
clients: [
14+
{
15+
client_id: '123',
16+
client_secret: 'somesecret',
17+
redirect_uris: [
18+
'https://jwt.io',
19+
'https://oauthdebugger.com/debug',
20+
'http://0.0.0.0:8910/redirect/oauth2_server_redwood',
21+
'https://oauth2-client-redwood-eta.vercel.app/redirect/node_oidc',
22+
],
23+
},
24+
],
25+
},
26+
})
27+
)

oauth-debugger.png

47.8 KB
Loading

packages/oauth2-server/src/config.js

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,20 @@ import getAdapter from './adapter'
33
import htmlSafe from './helpers'
44
import jwks from './jwks'
55

6-
export const getConfig = (db) => {
6+
export const getConfig = (db, settings) => {
77
const adapter = getAdapter(db)
88
return {
99
adapter,
1010
findAccount: findAccount(db),
11-
// clients: [
12-
// {
13-
// client_id: '123',
14-
// client_secret: 'somesecret',
15-
// redirect_uris: [
16-
// 'https://jwt.io',
17-
// 'http://0.0.0.0:3000/redirect/node_oidc',
18-
// 'http://0.0.0.0:8910/redirect/node_oidc',
19-
// 'http://localhost:8910/redirect/node_oidc',
20-
// 'http://0.0.0.0:8910/redirect/oauth2_server_redwood',
21-
// 'https://oauth2-client-redwood-eta.vercel.app/redirect/node_oidc',
22-
// ],
23-
// },
24-
// ],
2511
clientDefaults: {
2612
grant_types: ['authorization_code'],
2713
id_token_signed_response_alg: 'RS256',
2814
response_types: ['code'],
2915
token_endpoint_auth_method: 'client_secret_post',
3016
},
17+
// TODO: unsure if this config is needed
3118
// clientAuthMethods: ['client_secret_post'],
32-
cookies: { keys: process.env.SECURE_KEY.split(',') },
19+
cookies: { keys: settings.SECURE_KEY.split(',') },
3320
jwks,
3421
ttl: {
3522
// Sessions
@@ -67,19 +54,19 @@ export const getConfig = (db) => {
6754
},
6855

6956
features: {
70-
introspection: {
71-
enabled: true,
72-
introspectionAllowedPolicy: (ctx, client, token) => {
73-
return true
74-
if (
75-
client.clientAuthMethod === 'none' &&
76-
token.clientId !== ctx.oidc.client.clientId
77-
) {
78-
return false
79-
}
80-
return true
81-
},
82-
},
57+
// TODO: enable introspection for API server
58+
// introspection: {
59+
// enabled: true,
60+
// introspectionAllowedPolicy: (ctx, client, token) => {
61+
// if (
62+
// client.clientAuthMethod === 'none' &&
63+
// token.clientId !== ctx.oidc.client.clientId
64+
// ) {
65+
// return false
66+
// }
67+
// return true
68+
// },
69+
// },
8370
devInteractions: { enabled: false },
8471
},
8572
renderError: async (ctx, out, error) => {
@@ -103,5 +90,6 @@ export const getConfig = (db) => {
10390
</body>
10491
</html>`
10592
},
93+
...settings.config,
10694
}
10795
}

packages/oauth2-server/src/index.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import { dbAuthSession } from './shared'
1111
// const cors = require('cors')
1212
const Provider = require('oidc-provider')
1313

14-
const app = ({ db }) => {
15-
assert(process.env.SECURE_KEY, 'process.env.SECURE_KEY missing')
14+
const app = (db, settings) => {
15+
assert(settings.SECURE_KEY, 'settings.SECURE_KEY missing')
1616
assert.equal(
17-
process.env.SECURE_KEY.split(',').length,
17+
settings.SECURE_KEY.split(',').length,
1818
2,
19-
'process.env.SECURE_KEY format invalid'
19+
'settings.SECURE_KEY format invalid'
2020
)
2121

2222
const authenticate = async (req) => {
@@ -32,8 +32,8 @@ const app = ({ db }) => {
3232
}
3333

3434
const oidc = new Provider(
35-
`${process.env.APP_DOMAIN}/api/oauth`,
36-
getConfig(db)
35+
`${settings.APP_DOMAIN}/api/oauth`,
36+
getConfig(db, settings)
3737
)
3838
oidc.proxy = true
3939

@@ -66,7 +66,7 @@ const app = ({ db }) => {
6666
req.apiGateway?.event?.queryStringParameters?.login_provider
6767
if (prompt.name === 'login') {
6868
if (provider) {
69-
const response = await fetch(`${process.env.APP_DOMAIN}/api/auth`, {
69+
const response = await fetch(`${settings.APP_DOMAIN}/api/auth`, {
7070
method: 'POST',
7171
headers: { 'Content-Type': 'application/json' },
7272
body: JSON.stringify({
@@ -78,9 +78,10 @@ const app = ({ db }) => {
7878
if (!response.url) throw "Error during sign up. Couldn't fetch url."
7979
return res.redirect(response.url)
8080
}
81-
return res.redirect(`/signin?uid=${uid}`)
81+
return res.redirect(`${settings.routes.login}?uid=${uid}`)
8282
}
83-
return res.redirect(`/authorize?uid=${uid}`)
83+
// Interaction is not logging in, so we must be authorizing
84+
return res.redirect(`${settings.routes.authorize}?uid=${uid}`)
8485
} catch (err) {
8586
return next(err)
8687
}
@@ -232,4 +233,4 @@ const app = ({ db }) => {
232233
return expressApp
233234
}
234235

235-
export default ({ db }) => app({ db })
236+
export default (...args) => app(...args)

web/src/Routes.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ const Routes = () => {
1111
<Route path="/authorize" page={AuthorizePage} name="authorize" />
1212
<Route path="/" page={HomePage} name="home" />
1313
<Route path="/redirect/{type}" page={RedirectPage} name="redirect" />
14-
<Route path="/signin" page={SignInPage} name="signin" />
14+
<Route path="/login" page={LoginPage} name="login" />
1515
<Route notfound page={NotFoundPage} />
16-
<Private unauthenticated="signin">
16+
<Private unauthenticated="login">
1717
<Route path="/profile" page={ProfilePage} name="profile" />
1818
</Private>
1919
</Set>

web/src/layouts/DefaultLayout/DefaultLayout.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const DefaultLayout = ({ children, background }) => {
1616
{isAuthenticated ? (
1717
<button onClick={logOut}>Log Out</button>
1818
) : (
19-
<Link to={routes.signin()}>Sign In</Link>
19+
<Link to={routes.login()}>Sign In</Link>
2020
)}
2121
</div>
2222
</header>

web/src/pages/SignInPage/SignInPage.js renamed to web/src/pages/LoginPage/LoginPage.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const LoginPortal = () => {
9797
)
9898
}
9999

100-
const SignInPage = () => {
100+
const LoginPage = () => {
101101
return (
102102
<>
103103
<MetaTags title="Sign In" description="Join to start collecting." />
@@ -106,4 +106,4 @@ const SignInPage = () => {
106106
)
107107
}
108108

109-
export default SignInPage
109+
export default LoginPage
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import LoginPage from './LoginPage'
2+
3+
export const generated = () => {
4+
return <LoginPage />
5+
}
6+
7+
export default { title: 'Pages/LoginPage' }
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { render } from '@redwoodjs/testing'
22

3-
import SignInPage from './SignInPage'
3+
import LoginPage from './LoginPage'
44

5-
describe('SignInPage', () => {
5+
describe('LoginPage', () => {
66
it('renders successfully', () => {
77
expect(() => {
8-
render(<SignInPage />)
8+
render(<LoginPage />)
99
}).not.toThrow()
1010
})
1111
})

0 commit comments

Comments
 (0)