Skip to content

Commit

Permalink
First attempt at implementing 'OAuth2 for first party apps'
Browse files Browse the repository at this point in the history
  • Loading branch information
evert committed Jul 19, 2024
1 parent b4b4dbd commit 32a4bbc
Show file tree
Hide file tree
Showing 18 changed files with 498 additions and 48 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Changelog
* API clients can now request that one-time-tokens don't expire after use.
* The client_id is now validated to belong to the curent user when validating
one-time-tokens.
* Fixed result of one-time-token if a custom expiry was used.


0.25.0 (2023-11-22)
Expand Down
27 changes: 27 additions & 0 deletions schemas/authorization-challenge-request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://curveballjs.org/schemas/a12nserver/authorization-challenge-request.json",
"type": "object",
"title": "AuthorizationChallengeRequest",
"description": "Request body for the Authorization Challenge Request endpoint for first-party applications.",
"required": [],
"additionalProperties": false,

"properties": {
"scope": {
"type": "string",
"description": "OAuth2 scope"
},
"auth_session": {
"description": "If the client has started a login session, specify auth_session to continue the login process",
"type": "string"
},
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}

}
22 changes: 22 additions & 0 deletions src/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@ export interface App {
* and run json-schema-to-typescript to regenerate this file.
*/

/**
* Request body for the Authorization Challenge Request endpoint for first-party applications.
*/
export interface AuthorizationChallengeRequest {
/**
* OAuth2 scope
*/
scope?: string;
/**
* If the client has started a login session, specify auth_session to continue the login process
*/
auth_session?: string;
username?: string;
password?: string;
}
/* eslint-disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/

/**
* This is the request body used by the HTML form submission for creating new groups
*/
Expand Down
5 changes: 1 addition & 4 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import accessLog from '@curveball/accesslog';
import mainMw from './main-mw.js';
import { init as initDb } from './database.js';
import { load } from './server-settings.js';
import './env.js';

import { NAME, VERSION } from './version.js';

import * as dotenv from 'dotenv';

dotenv.config();

console.info('⚾ %s %s', NAME, VERSION);

const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 8531;
Expand Down
4 changes: 1 addition & 3 deletions src/database.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/* eslint no-console: 0 */
import { Knex, default as knex } from 'knex';
import * as path from 'node:path';
import * as dotenv from 'dotenv';
import { fileURLToPath } from 'node:url';

dotenv.config();
import './env.js';

let settings: Knex.Config | null = null;
const db: Knex = knex(getSettings());
Expand Down
18 changes: 18 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

/**
* This file exists to ensure that .env is loaded as early as possible.
* Anything that needs it should import it
*/
import * as dotenv from 'dotenv';

if (process.env.PUBLIC_URI === undefined) {
// If there's no PUBLIC_URI environment variable, it's a good indication
// that we may be missing a .env file.
//
// This is the only required environment variable.
dotenv.config({path: dirname(fileURLToPath(import.meta.url)) + '/../.env'});
} else {
console.warn('/env.js was loaded twice?');
}
3 changes: 3 additions & 0 deletions src/home/formats/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ OAuth2 endpoints
* <a href="/token" rel="token">Token endpoint</a>
* <a href="/.well-known/oauth-authorization-server" rel="oauth_server_metadata_uri">Authorization Server Metadata</a> ([RFC8414][RFC8414])
* <a href="/.well-known/jwks.json" rel="jwks">JSON Web Key Sets</a> ([RFC7517][RFC7517])
* <a href="/authorization-challenge" rel="OAuth 2.0 for First-Party Applications">Authorization Challenge Endpoint</a> ([draft][draft-parecki-oauth-first-party-apps]) <span class="link-badge status-experimental">Experimental</span>
Other API endpoints
-------------------
Expand All @@ -75,6 +77,7 @@ _Version ${version}_
[RFC7662]: https://tools.ietf.org/html/rfc7662 "OAuth 2.0 Token Introspection"
[RFC8414]: https://tools.ietf.org/html/rfc8414 "OAuth 2.0 Authorization Server Metadata"
[RFC7517]: https://tools.ietf.org/html/rfc7517 "JSON Web Key (JWK)"
[draft-parecki-oauth-first-party-apps]: https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-00.html "OAuth 2.0 for First-Party Applications"
`;

};
12 changes: 1 addition & 11 deletions src/knexfile.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
import { dirname } from 'node:path';
import { getSettings } from './database.js';
import * as dotenv from 'dotenv';
import { fileURLToPath } from 'node:url';

if (process.env.PUBLIC_URI === undefined) {
// If there's no PUBLIC_URI environment variable, it's a good indication
// that we may be missing a .env file.
//
// This is the only required environment variable.
dotenv.config({path: dirname(fileURLToPath(import.meta.url)) + '/../.env'});
}
import './env.js';

const settings = getSettings();

Expand Down
59 changes: 59 additions & 0 deletions src/login/controller/authorization-challenge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Controller } from '@curveball/controller';
import { Context } from '@curveball/kernel';
import { getOAuth2ClientFromBasicAuth } from '../../app-client/service.js';
import { UnauthorizedClient } from '../../oauth2/errors.js';
import { UnsupportedMediaType } from '@curveball/http-errors';
import { AuthorizationChallengeRequest } from '../../api-types.js';
import * as loginService from '../service.js';

/**
* The authorization challenge controller is an implementation of OAuth 2.0
* for First-Party Applications.
*
* This specification is currently a draft, and so this endpoint will also
* evolve as the specification evolves.
*
* The endpoint lets a first party, trusted app authenticate with an OAuth2
* server and completely own UI.
*
* It's implemented as a series of requests and challenges until all
* challenges are met. For example, a first challenge may be a password, a
* second a TOTP token. Once there are no more challenges, a 'code' gets
* returned which may be exchanged for a 'token' on the token endpoint.
*
* https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-00.html
*
*/
class AuthorizationChallengeController extends Controller {

async post(ctx: Context) {

// We will only support Basic auth for now.
const client = await getOAuth2ClientFromBasicAuth(ctx);

if (!client.allowedGrantTypes.includes('authorization_challenge')) {
throw new UnauthorizedClient('This client is not allowed to use the "authorization_challenge" oauth2 flow');
}

if (!ctx.request.is('application/x-www-form-urlencoded')) {
throw new UnsupportedMediaType('This endpoint requires thei request to use the "application/x-www-form-urlencoded" content-type');
}

ctx.request.validate<AuthorizationChallengeRequest>('https://curveballjs.org/schemas/a12nserver/authorization-challenge-request.json');

const request = ctx.request.body;

const session = await loginService.getSession(client, request);
const code = await loginService.challenge(client, session, request);

ctx.response.body = {
authorization_code: code.code,
};

}

}



export default new AuthorizationChallengeController();
Loading

0 comments on commit 32a4bbc

Please sign in to comment.