Skip to content

Commit

Permalink
Start documenting.
Browse files Browse the repository at this point in the history
  • Loading branch information
kohler committed Oct 16, 2023
1 parent 779c238 commit b9bd848
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 15 deletions.
109 changes: 109 additions & 0 deletions devel/manual/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# HotCRP components

HotCRP features including page renderers, search keywords, submission and
review field types, and formula functions are configured through JSON
components. You can add to, modify, and selectively disable components for
your conference by declaring options in `conf/options.php`.

## Component types

The components for each feature are defined by a JSON configuration file in
`etc/`, and then modified by JSON objects and files listed in an `$Opt`
setting.

| Feature | Default configuration file | `$Opt` setting | Match? | User `allow_if`? |
| ----------------------------------------- | ----------------------------- | ------------------------- | ------ | ----------- |
| Request handling and page rendering | `etc/pages.json` | `$Opt["pages"]` | ||
| API endpoints | `etc/apifunctions.json` | `$Opt["apiFunctions"]` | ||
| Formula functions | `etc/formulafunctions.json` | `$Opt["formulaFunctions"]` | ||
| Submission field types | `etc/optiontypes.json` | `$Opt["optionTypes"]` | | |
| Sample submission fields | `etc/submissionfieldlibrary.json` | `$Opt["submissionFieldLibraries"]` | | |
| Assignment types for bulk assignment | `etc/assignmentparsers.json` | `$Opt["assignmentParsers"]` | ||
| Autoassigners | `etc/autoassigners.json` | `$Opt["autoassigners"]` | ||
| Help topics | `etc/helptopics.json` | `$Opt["helpTopics"]` | ||
| Search actions | `etc/listactions.json` | `$Opt["listActions"]` | ||
| Mail keywords | `etc/mailkeywords.json` | `$Opt["mailKeywords"]` |||
| Mail templates | `etc/mailtemplates.json` | `$Opt["mailTemplates"]` | ||
| Search columns | `etc/papercolumns.json` | `$Opt["paperColumns"]` |||
| Profile page topics | `etc/profilegroups.json` | `$Opt["profileGroups"]` | ||
| Sample review fields | `etc/reviewfieldlibrary.json` | `$Opt["reviewFieldLibraries"]` | ||
| Review field types | `etc/reviewfieldtypes.json` | `$Opt["reviewFieldTypes"]` | | |
| Search keywords | `etc/searchkeywords.json` | `$Opt["searchKeywords"]` |||
| Setting topics and rendering | `etc/settinggroups.json` | `$Opt["settingGroups"]` | ||
| Settings | `etc/settinginfo.json` | `$Opt["settingInfo"]` | ||
| OAuth/OpenID authentication types | None | `$Opt["oAuthTypes"]` | | |

## Component construction

HotCRP creates a feature’s components by **expanding** the default
configuration file and `$Opt` setting into a single list of **component
fragments**, which are named objects. To look up a component by name, HotCRP
**searches** this list for matching objects, then **merges** those matches
together to create a single component.

## Expansion: `$Opt` setting format

Each `$Opt` setting is an optional list of entries, where an entry is one of:

* A single PHP object (which is a component fragment).
* A list of PHP objects (component fragments).
* A JSON string that defines either a single object or an array of objects.
* A filename, which should contain a JSON array of objects.
(HotCRP will report an error if the file can’t be found.)
* A filename preceded by `?`. (HotCRP will *not* report an error if the
filename can’t be found.)
* A filename pattern, including wildcard characters like `*`, `?`, and
`[...]`. (HotCRP will read all matching files.)

Filenames are searched for in the HotCRP directory and then using the
`$Opt["includePath"]` setting, which should be a list of directories.

## Search: Names and matches

Components are defined by name, and every component fragment created by the
expansion process should have a `name` property, which is a string. When looking
up a component, HotCRP first filters the list of fragments for those with
matching `name`.

Some component types, such as search keywords, can be constructed on demand.
These support **pattern fragments**, which are component fragments with a
`match` property instead of `name`. When looking up a component, HotCRP
includes all pattern fragments whose `match` property matches the desired name
as a regular expression.

A pattern fragment can have an `expand_function` property. When such a pattern
fragment matches a desired name, HotCRP calls that PHP function, passing as
arguments (1) the desired name, (2) an `XtParams` object, (3) the pattern
fragment, and (4) the `preg_match` data. The function must return a list of
component fragments (which might be empty).

## Merging

The search process yields a list of component fragments for the desired name.
HotCRP then merges those components together into a single component, which is
returned. The merge process works as follows.

1. The list of fragments is sorted by increasing `priority`. `priority`
defaults to 0; fragments with equal `priority` are ordered by their
position in the original fragment list (so, for example, fragments from
`$Opt` generally override fragments from `etc/`).

2. Partial fragments are merged. A partial fragment has a `merge` property set
to `true`; such fragments do not stand on their own, but modify preceding
fragments by overriding their properties. For example, the partial fragment
`{"name":"foo", "priority":2, "merge":true, "title":"Foo"}` will change the
`title` property of some component without affecting its other properties.

Once partial fragments are merged, the list contains complete components.

3. Disallowed components are dropped. A component can be restricted to
specific users or conferences by setting its `allow_if` property. For
instance, a component with `"allow_if":"admin"` is only available to
administrators. A component with `"allow_if":false` is available to no one.

A few features do not support per-user components, but `allow_if` can
still be used to check selected conference properties, as in
`"allow_if":"conf.external_login"`.

4. The highest-priority remaining component, if any, is the result of the
merge process.
70 changes: 70 additions & 0 deletions devel/manual/oauth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# HotCRP OAuth

Configure HotCRP’s `$Opt["oAuthTypes"]` setting in `conf/options.php` to use
[OAuth 2.0][OAuth] and [OpenID Connect][] to authenticate users.

## `oAuthTypes` format

The `oAuthTypes` option is a list of [components][] defining supported OAuth
authentication providers. Each `oAuthTypes` component should define:

* `name`: The name of the provider. Each provider must have a distinct name.
Internal to HotCRP. Example: `"Google"`

* `title`: (Optional) A short description of the authentication provider, to
be used in error messages. Defaults to `name`.

* `client_id`, `client_secret`: Your client ID and secret. These are sent to
the authentication provider as part of the authentication process.

* `auth_uri`: The provider’s authentication URI. Example:
`"https://accounts.google.com/o/oauth2/v2/auth"`

* `token_uri`: The provider’s URI for fetching authentication results.
Example: `"https://oauth2.googleapis.com/token"`

* `redirect_uri`: (Optional) The HotCRP URI registered with the provider.
Defaults to `SITEURL/oauth`.

* `scope`: (Optional) The OAuth scopes to be requested as part of the
authentication process. Defaults to `"openid email profile"`

* `token_function`: (Optional) PHP callback to be called after a token is
returned, but before HotCRP validates the token.

* `button_html`: HTML contents of the signin button for this provider. If
empty, then HotCRP does not display a signin button. Example: `"Sign in with
Google"`

* `disabled`: (Optional) If true, HotCRP disables this provider.

## Authentication flow

HotCRP’s page component `"signin/form/oauth"` renders a button for each
defined provider. Clicking on that button redirects to
`SITEURL/oauth?authtype=NAME`. That page initiates an OAuth 2 implicit
authentication flow by choosing a random token, recording it, and redirecting
the user to the specified `auth_uri` with appropriate parameters. When the
user completes their authentication request, the provider redirects back to
HotCRP via the `redirect_uri`. HotCRP contacts the provider’s `token_uri` with
the provided parameters via an HTTP `POST` request with
`application/x-www-form-urlencoded` content. HotCRP then validates the
returned JWT and uses its included `email` to authenticate the user.

Many steps in this process might go wrong. HotCRP uses its own code to
validate the JWT; this might break. Report problems to maintainers.

HotCRP does not currently cryptographically validate the returned token’s
signature.

## Disabling other authentication sources

Set `$Opt["loginType"]` to `"oauth"` to use *only* OAuth to authenticate
users. If `$Opt["loginType"]` is `"oauth"` or `"none"`, then HotCRP will not
use its own password storage or allow attempts to sign in other than through
OAuth.


[OAuth]: https://en.wikipedia.org/wiki/OAuth]
[OpenID Connect]: https://en.wikipedia.org/wiki/OpenID
[components]: ./components.md
30 changes: 15 additions & 15 deletions src/pages/p_oauth.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class OAuthInstance {
/** @var string */
public $authtype;
public $name;
/** @var ?string */
public $title;
/** @var ?string */
Expand All @@ -20,36 +20,36 @@ class OAuthInstance {
/** @var string */
public $token_uri;
/** @var ?string */
public $load_function;
public $token_function;
public $require;

function __construct($authtype) {
$this->authtype = $authtype;
function __construct($name) {
$this->name = $name;
}

/** @param Conf $conf
* @param ?string $authtype
* @param ?string $name
* @return ?OAuthInstance */
static function find($conf, $authtype) {
static function find($conf, $name) {
$authinfo = $conf->oauth_types();
if (empty($authinfo)) {
return null;
}
if ($authtype === null) {
$authtype = (array_keys($authinfo))[0];
if ($name === null) {
$name = (array_keys($authinfo))[0];
}
if (!($authdata = $authinfo[$authtype] ?? null)) {
if (!($authdata = $authinfo[$name] ?? null)) {
return null;
}
$instance = new OAuthInstance($authtype);
$instance = new OAuthInstance($name);
$instance->title = $authdata->title ?? null;
$instance->scope = $authdata->scope ?? null;
$instance->client_id = $authdata->client_id ?? null;
$instance->client_secret = $authdata->client_secret ?? null;
$instance->auth_uri = $authdata->auth_uri ?? null;
$instance->token_uri = $authdata->token_uri ?? null;
$instance->redirect_uri = $authdata->redirect_uri ?? $conf->hoturl("oauth", null, Conf::HOTURL_RAW | Conf::HOTURL_ABSOLUTE);
$instance->load_function = $authdata->load_function ?? null;
$instance->token_function = $authdata->token_function ?? null;
$instance->require = $authdata->require ?? null;
foreach (["client_id", "client_secret", "auth_uri", "token_uri", "redirect_uri", "title"] as $k) {
if (!is_string($instance->$k) && ($k !== "title" || $instance->$k !== null))
Expand Down Expand Up @@ -81,7 +81,7 @@ function start() {
->set_expires_after(60)
->set_token_pattern("hcoa[20]");
$tok->data = json_encode_db([
"authtype" => $authi->authtype,
"authtype" => $authi->name,
"session" => $this->qreq->qsid(),
"site_uri" => $this->conf->opt("paperSite")
]);
Expand Down Expand Up @@ -131,7 +131,7 @@ function response() {
* @param object $jdata */
private function instance_response($authi, $tok, $jdata) {
// make authentication request
$authtitle = $authi->title ?? $authi->authtype;
$authtitle = $authi->title ?? $authi->name;
$tok->delete();
$curlh = curl_init();
$nonce = base48_encode(random_bytes(10));
Expand Down Expand Up @@ -171,9 +171,9 @@ private function instance_response($authi, $tok, $jdata) {
return MessageItem::error("<0>The identity portion of the {$authtitle} authentication response doesn’t validate");
}

if (isset($authi->load_function)
if (isset($authi->token_function)
&& Conf::xt_resolve_require($authi)
&& ($m = call_user_func($authi->load_function, $this, $authi, $response, $jid))) {
&& ($m = call_user_func($authi->token_function, $this, $authi, $response, $jid))) {
return $m;
}

Expand Down

0 comments on commit b9bd848

Please sign in to comment.