Use one OAuth2 service as a proxy for many more.
That's the general idea. The implementation though, in the terms of security, is a little more complex.
For now, this project implements 2 methods of authentication, Discord and Twitter (more are being added).
Twitter represents the OAuth 1.0a authentication flow and Discord the OAuth 2.0 authentication flow.
Due to the severe implications of a possible programming error or a vulnerability in a project like this, this project will remain as a demo until further notice. DO NOT USE IT IN ANY PRODUCTION ENVIRONMENT!
You can test the application directly at id.testausserveri.fi
and the example client implementation at idexample.testausserveri.fi
.
You should read these before making an issue. These will be fixed/implemented asap.
- Port over the other 8 login methods from the prototype
- Code audit
- Cybersecurity assessment
- Bug bounty?
- Token revocation after authentication
This is the draft documentation for this project.
This project's backend is developed with Node.Js v16.13.2
and uses Docker
and Docker-compose
for development and deployment. MongoDB
functions as the backend database. Major dependencies are the mongoose
and dotenv
packages from the NPM-registry
, otherwise this project only uses native Node.Js libraries.
The front-end is rather bare-bones and is developed with pure HTML5
, CSS
and JavaScript
.
Relevant configuration for the front-end exists in /src/app/branding.json
. This is the configuration for the front-end application's themes. All colors and relevant text is configurable.
Login methods are configured with /src/methods.json
. The only notable thing about the structure is that the credentials object's key's values are environment variable names.
Environment variables are configured with the .env
file. See .env.example
for more details on what variables are required.
This project implements a rather simple CLI to create, update and remove application registrations, though these details can be updated directly to the database by some other program as long as they follow the specified database schemas.
Command usage and definitions can be viewed with the help
command.
Example output
Commands:
- "exit", closes the program.
- "help", displays this message.
- "createApplication <MongoDB JSON query>", create an application.
- name: string
- homepage: string
- icon: base64 image string
- redirectURLS: string[]
- "updateApplication <MongoDB JSON query>", update an application.
- identical with createApplication
- "getApplications", get all applications.
- "removeApplication <MongoDB JSON query>", remove an application.
- id: string
src
├───app
├───database
├───methods
│ └───api
│ ├───_callbacks
│ └───_preflights
├───typings
└───util
- The
app
directory contains the front-end application for method selection during login. - The
database
directory contains all database-related scripting and schemas. - The
methods
directory is a collection of dynamically loaded webserver methods. A template for these methods is available in/dev
. The directory structure of themethods
directory does not in any way reflect how the methods are loaded. It exists only for the sake of organization of files. - The
typings
folder includes all the*.ts
typings for IntelliSense (etc.) - The
util
directory includes various smaller scripts and libraries used across this project's source code.
This project uses docker-compose
for deployment and testing.
You can run the project with docker-compose up
.
To run the project without docker-compose
, you need to have Node v16.13.2
and NPM v8.1.2
or later installed (older version may work) and follow these steps:
- Install all dependencies with
npm install
- Define
DATABASE_URL
in the.env
as the URL of theMongoDB
database. - Run the project with
npm start
The API root is: /api/v1/
Methods marked with (private)
are only used internally.
API v1 implements a standard OAuth 2.0
authentication service, with the following configuration:
GET /api/v1/authenticate
as the authorization endpoint.POST /api/v1/token
as the token retrieval endpoint.
Error format
{ "error": "<error description>" }
The "error" value will never be included in the response if it was successful.
Available scopes
- "token", pass through the account accessToken from the authentication flow (high risk)
- "id", include the user-id in the /api/v1/me response
- "account", include general account details, like the account name and icon in the /api/v1/me response
- "contact", include account email and/or phone number in the /api/v1/me response
- "security", include relevant information about the account security configuration in the /api/v1/me response
At least one scope must always be included
Example flow of authentication with v1's code flow
[CLIENT]
The user is redirected to/api/v1/authenticate?...
from the client application.- A new authentication session is created in the backend (stage:
created
)*
- A new authentication session is created in the backend (stage:
- The user is automatically redirected to
/app?state=...
- The user selects the platform to authenticate with.
- The user is redirected to
/api/v1/login?platform=...&state=...
- Authentication stage changes in the backend (state:
pending
)*
- Authentication stage changes in the backend (state:
- The user is automatically redirected to platform's login service.
- The user is redirected from the platform login to
/api/v1/callback
after authenticating.- Authentication stage changes (stage:
completed
)*
- Authentication stage changes (stage:
[CLIENT]
The user is redirected back to client application's callback URL withcode=...
in the request query.[CLIENT]
The client application makes a request to/api/v1/token?code=...
and gets token- Authentication stage changes in the backend (stage:
stored
)*
- Authentication stage changes in the backend (stage:
[CLIENT]
The client application makes a request to/api/v1/me
with token in the header:Authentication: Bearer <token>
- Authentication session is deleted
--> User has now been authenticated
*An authentication stage change defines a point of no return for the authentication flow
Steps prefixed with [CLIENT]
are the clients responsibility
Exchange the "code" for an access token to access the authenticated user's information.
Request requirements
- Header "Content-Type" must be "application/x-www-form-urlencoded"
- Body must include parameters "code", "grant_type", "redirect_uri", "client_id" and "client_secret".
- "grant_type" must be "authorization_code"
- "redirect_uri" must match with the uri used before with /authenticate
Example response
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 07 Mar 2022 09:46:17 GMT
Connection: close
Content-Length: ...
{ "token": "...", "expiry": "<ms>" }
Retrieve information about a registered application. Displayed during the login process.
Request requirements
- The request query parameters must contain "client_id" as the application's ID to query.
Example response
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 07 Mar 2022 09:46:17 GMT
Connection: close
Content-Length: 107
{ "id": "d3c89442d3574aa5bbaea011f2d43e14", "name": "Test application", "icon": "","homepage": "http://localhost" }
Retrieve a list of available authentication methods.
Example response
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 07 Mar 2022 09:46:16 GMT
Connection: close
Content-Length: 193
[
{
"name": "Discord",
"id": "2db260c7-8ca9-42a3-8de8-a6a3c37be89e",
"icon": "/app/assets/Discord.svg"
},
{
"name": "Twitter",
"id": "ba8aad4d-9014-4ecc-9df3-e2d520b4c23e",
"icon": "/app/assets/Twitter.svg"
}
]
Begin the authentication flow by creating a new authentication session.
Request requirements
- Request query must include
response_type=code
.- An alternate
token
response type exists in the V2 API (documentation below).
- An alternate
- Request query must contain valid
client_id
,redirect_uri
andscope
parameters (more about scopes above).
Optional
- The request query may include the
state
query parameter to identify the session callback later on in the authentication flow. - The request query may include the
noRedirect
query parameter in which case the server response will not a a redirect (307), but instead a normal 200 response with the location header's name changed tox-Location
.
Example response
HTTP/1.1 307 Temporary Redirect
Location: /app?scopes=token,id,account,contact,security&client_id=d3c89442d3574aa5bbaea011f2d43e14&state=572dd500b8c73b26b00a45693336058c&redirect_uri=http://localhost/callback
Content-Type: text/html
Date: Mon, 07 Mar 2022 09:46:15 GMT
Connection: close
Content-Length: 218
If you are not redirected click <a href="/app?scopes=token,id,account,contact,security&client_id=d3c89442d3574aa5bbaea011f2d43e14&state=572dd500b8c73b26b00a45693336058c&redirect_uri=http://localhost/callback">this</a>.
Redirect the user to the method login.
Request requirements
- The request query must include valid
state
(redirectId) andmethod
(id) parameters.
Example response
HTTP/1.1 307 Temporary Redirect
Location: https://discord.com/api/oauth2/authorize?...
Content-Type: text/html
Date: Mon, 07 Mar 2022 09:46:18 GMT
Connection: close
Content-Length: ...
If you are not redirected, click <a href="https://discord.com/api/oauth2/authorize?...">here</a>.
<br>
<i>(https://discord.com/api/oauth2/authorize?...)</i>
The callback url from the selected authentication method. Request requirements are method specific. Responses are method specific. See /src/methods/api/_callbacks/
.
Get information about the logged-in user.
Request requirements
- The request headers must include the account access token as "Bearer" (
Bearer <token>
).
Example response
HTTP/1.1 200 OK
Content-Type: text/html
Date: Mon, 07 Mar 2022 09:46:18 GMT
Connection: close
Content-Length: ...
{
"?token": "<Account access token>",
"?id": "<Account ID>",
"?name": "<Account name>",
"?account": "<General account information, platform specific>",
"?security": "<Account security information, platform specific>",
"?contact": "<Account contact details, platform specific>",
"scopes": "String[]",
"applicationId": "<Application ID>",
"platform": {
"id": "<Platform ID>",
"name": "<Platform name>"
}
}
The fields prefixed with ? are present/missing depending on what scopes were used. See available scopes above.
Implements the same configuration as API V1. Only difference is the supported methods.
Example flow of authentication with v2's token flow
[CLIENT]
The user is redirected to/api/v2/authenticate?...
from the client application.- A new authentication session is created in the backend (stage:
created
)*
- A new authentication session is created in the backend (stage:
- The user is automatically redirected to
/app?state=...
- The user selects the platform to authenticate with.
- The user is redirected to
/api/v1/login?platform=...&state=...
- Authentication stage changes in the backend (state:
pending
)*
- Authentication stage changes in the backend (state:
- The user is automatically redirected to platform's login service.
- The user is redirected from the platform login to
/api/v1/callback
after authenticating.- Authentication stage changes (stage:
completed
)*
- Authentication stage changes (stage:
[CLIENT]
The user is redirected back to client application's callback URL withtoken=...
in the request query and gets the token.- Authentication stage changes in the backend (stage:
stored
)*
- Authentication stage changes in the backend (stage:
[CLIENT]
The client application makes a request to/api/v1/me
with token in the header:Authentication: Bearer <token>
- Authentication session is deleted
--> User has now been authenticated
*An authentication stage change defines a point of no return for the authentication flow
Steps prefixed with [CLIENT]
are the clients responsibility
Create an authentication session with scopes and allowed methods configured server-side.
This somewhat imitates the OAuth 1.0a authorization flow.
Request requirements
- The request headers must include the client secret token as "Bearer" (
Bearer <token>
). - Header "Content-Type" must be "application/x-www-form-urlencoded"
- The request body must include parameters
- redirect_uri
- scope (authentication scopes)
- methods (list of method IDs see /api/v1/methods for more details)
Optional
- The request body may include the
state
parameter to identify the session callback later on in the authentication flow.
Example response
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 07 Mar 2022 09:46:17 GMT
Connection: close
Content-Length: ...
{ "oauth_token": "..." }
Continue the authentication flow after manual session creation to the "created" stage.
Request requirements
- Request query must contain valid
oauth_token
. If nooauth_token
is provided, see next requirement. - (If no
oauth_token
is provided) Request query must containresponse_type=token
.- Therefore, the request must contain valid
client_id
,redirect_uri
andscope
parameters (more about scopes above).
- Therefore, the request must contain valid
Optional
- The request query may include the
state
query parameter to identify the session callback later on in the authentication flow. - The request query may include the
noRedirect
query parameter in which case the server response will not a a redirect (307), but instead a normal 200 response with the location header's name changed tox-Location
.
Example response
HTTP/1.1 307 Temporary Redirect
Location: /app?scopes=token,id,account,contact,security&client_id=d3c89442d3574aa5bbaea011f2d43e14&state=572dd500b8c73b26b00a45693336058c&redirect_uri=http://localhost/callback
Content-Type: text/html
Date: Mon, 07 Mar 2022 09:46:15 GMT
Connection: close
Content-Length: 218
If you are not redirected click <a href="/app?scopes=token,id,account,contact,security&client_id=d3c89442d3574aa5bbaea011f2d43e14&state=572dd500b8c73b26b00a45693336058c&redirect_uri=http://localhost/callback">this</a>.
About OAuth: https://aaronparecki.com/oauth-2-simplified/
- created (after /authenticate call)
- pending (after platform has called back, user redirected)
- completed (after callback)
- stored (token given, pending for deletion)
Errors thrown by methods, which begin with safe:
, will have their message as a string in the response.
Verifying the user used the same platform as before to authenticate is essential. Could we somehow force this check to take place? Like including the platform id within the user id? Or the platform name?
Implementing a new authentication method should be a trivial task. Here's a step-by-step guide on how to do that.
-
First, create the callback in
src/methods/api/_callbacks/
. The name of the file has to be the method<name>.js?
.- A template for callbacks can be found in
dev/methodExample.js
- A template for callbacks can be found in
-
Write the method configuration to
src/methods.json
Example
{ "name": "Example", "prettyName": "Optional pretty name with spaces and stuff", "id": "<Random uuidV4>", "icon": "/app/assets/<name>.svg", "url": "https://example.com/login/oauth/authorize?scope=${scopes}&client_id=${process.env.EXAMPLE_CLIENT_ID}&redirect_uri=${redirectURI}%3Fstate=${state}", "credentials": { "clientId": "EXAMPLE_CLIENT_ID", "secret": "EXAMPLE_CLIENT_SECRET" } }
The credential's fields values are the names of the environment variables used to store the credentials in
.env
. The keys of these fields define what name you will use with thegetCredentials
utility. In this case you can read the clientId at runtime withgetCredentials("Example", "clientId")
.The url is the OAuth server URL for the method to be added. For all the variables, look at the list further below.
The icon field's value is the location of the method icon accessible in the
src/app
directory. The icon is directly used in the img element's source, so no special format restrictions apply. -
Implement scope conversions in
src/util/getScopes
. Configuration is at the top of the file (this may change soon).- Include the character(s) that separates scopes in the
scopeSeparator
object, when multiple scopes are used. - Include direct scope conversions in the
scopeConversion
object. These are direct key->value conversions to convert Testausserveri ID scopes to the ones the specific method wants to access the appropriate data. Using the least permissive scopes during conversion is recommended.
- Include the character(s) that separates scopes in the
-
Include the credentials in the
.env
files with the names you specified before insrc/methods.json
...and that's it!
${scopes}
, authentication scopes generated by scope conversions${process.env.<name>}
, a dynamic way to input any environment variable to the url. Especially useful for avoiding static client ids in the url.${redirectURI}
, the Testausserveri ID redirect uri.${state}
, the required authentication state code defined by Testausserveri ID.
Contributions are welcome. A few requirements:
- Read through this document
- Use conventional commits
- Use ESLint to enforce code-style and best practices
Copyright 2022 Testausserveri ry
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.