fix(monorepo): Replace code with stub README and docs pointers.
This commit is contained in:
Родитель
8453f6ec94
Коммит
f7025a40d1
|
@ -0,0 +1,6 @@
|
|||
# Firefox Accounts OAuth Server
|
||||
|
||||
This code is now managed in
|
||||
the [fxa-auth-server](https://github.com/mozilla/fxa-auth-server) repository.
|
||||
This repository is maintained for historical purposes only.
|
||||
|
|
@ -0,0 +1 @@
|
|||
This documentation now lives [in the auth-server repo](https://github.com/mozilla/fxa-auth-server/blob/master/fxa-oauth-server/docs/api.md).
|
|
@ -0,0 +1 @@
|
|||
This documentation now lives [in the auth-server repo](https://github.com/mozilla/fxa-auth-server/blob/master/fxa-oauth-server/docs/clients.md).
|
|
@ -0,0 +1 @@
|
|||
This documentation now lives [in the auth-server repo](https://github.com/mozilla/fxa-auth-server/blob/master/fxa-oauth-server/docs/pkce.md).
|
|
@ -0,0 +1 @@
|
|||
This documentation now lives [in the auth-server repo](https://github.com/mozilla/fxa-auth-server/blob/master/fxa-oauth-server/docs/scopes.md).
|
|
@ -0,0 +1 @@
|
|||
This documentation now lives [in the auth-server repo](https://github.com/mozilla/fxa-auth-server/blob/master/fxa-oauth-server/docs/service-clients.md).
|
|
@ -1,95 +0,0 @@
|
|||
# These environment variables must be set in CircleCI UI
|
||||
#
|
||||
# DOCKERHUB_REPO - docker hub repo, format: <username>/<repo>
|
||||
# DOCKER_EMAIL - login info for docker hub
|
||||
# DOCKER_USER
|
||||
# DOCKER_PASS
|
||||
#
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
|
||||
- run:
|
||||
name: Create version.json
|
||||
command: >
|
||||
printf '{"version":{"hash":"%s","version":"%s","source":"https://github.com/%s/%s","build":"%s"}}\n'
|
||||
"$CIRCLE_SHA1"
|
||||
"$CIRCLE_TAG"
|
||||
"$CIRCLE_PROJECT_USERNAME"
|
||||
"$CIRCLE_PROJECT_REPONAME"
|
||||
"$CIRCLE_BUILD_URL"
|
||||
| tee config/version.json version.json
|
||||
- store_artifacts:
|
||||
path: version.json
|
||||
|
||||
- run:
|
||||
name: Build deployment container image
|
||||
command: docker build -f Dockerfile-build -t fxa-oauth-server:build .
|
||||
|
||||
- run:
|
||||
name: Check npm install
|
||||
command: docker run --rm -it fxa-oauth-server:build npm ls --production
|
||||
|
||||
- run:
|
||||
name: Build test container image
|
||||
command: docker build -f Dockerfile-test -t fxa-oauth-server:test .
|
||||
|
||||
- run:
|
||||
name: Run MySQL
|
||||
command: docker run -d --name=mydb -e MYSQL_ALLOW_EMPTY_PASSWORD=true -e MYSQL_ROOT_HOST=% -p 3306:3306 mysql/mysql-server:5.6
|
||||
background: true
|
||||
|
||||
- run:
|
||||
name: Run Memory DB Tests
|
||||
command: docker run fxa-oauth-server:test npm test
|
||||
|
||||
- run:
|
||||
name: Run MySQL DB Tests
|
||||
command: docker run --net=host -p 3306:3306 -e DB="mysql" fxa-oauth-server:test npm test
|
||||
|
||||
- run:
|
||||
name: Push to Dockerhub
|
||||
command: |
|
||||
if [ "${CIRCLE_BRANCH}" == "master" ]; then
|
||||
DOCKER_TAG="latest"
|
||||
fi
|
||||
|
||||
if [[ "${CIRCLE_BRANCH}" == feature* ]] || [[ "${CIRCLE_BRANCH}" == dockerpush* ]]; then
|
||||
DOCKER_TAG="${CIRCLE_BRANCH}"
|
||||
fi
|
||||
|
||||
if [ -n "${CIRCLE_TAG}" ]; then
|
||||
DOCKER_TAG="$CIRCLE_TAG"
|
||||
fi
|
||||
|
||||
if [ -n "${DOCKER_TAG}" ]; then
|
||||
echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin
|
||||
echo ${DOCKERHUB_REPO}:${DOCKER_TAG}
|
||||
docker tag fxa-oauth-server:build ${DOCKERHUB_REPO}:${DOCKER_TAG}
|
||||
docker images
|
||||
docker push ${DOCKERHUB_REPO}:${DOCKER_TAG}
|
||||
fi
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
# workflow jobs are _not_ run in tag builds by default
|
||||
# we use filters to whitelist jobs that should be run for tags
|
||||
|
||||
# workflow jobs are run in _all_ branch builds by default
|
||||
# we use filters to blacklist jobs that shouldn't be run for a branch
|
||||
|
||||
# see: https://circleci.com/docs/2.0/workflows/#git-tag-job-execution
|
||||
|
||||
build-test-push:
|
||||
jobs:
|
||||
- build:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
|
@ -1 +0,0 @@
|
|||
.git
|
|
@ -1,10 +0,0 @@
|
|||
plugins:
|
||||
- fxa
|
||||
extends: plugin:fxa/server
|
||||
|
||||
rules:
|
||||
handle-callback-err: 0
|
||||
semi: [2, "always"]
|
||||
|
||||
parserOptions:
|
||||
ecmaVersion: 2017
|
|
@ -1,7 +0,0 @@
|
|||
npm-debug.log
|
||||
node_modules
|
||||
Thumbs.db
|
||||
/coverage.html
|
||||
config/key.json
|
||||
config/oldKey.json
|
||||
.nyc_output
|
|
@ -1,46 +0,0 @@
|
|||
language: node_js
|
||||
|
||||
node_js:
|
||||
- '8'
|
||||
|
||||
dist: trusty
|
||||
sudo: true
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
- mysql-server-5.6
|
||||
- mysql-client-core-5.6
|
||||
- mysql-client-5.6
|
||||
|
||||
notifications:
|
||||
email:
|
||||
smcarthur@mozilla.com
|
||||
jrgm@mozilla.com
|
||||
irc:
|
||||
channels:
|
||||
- 'irc.mozilla.org#fxa-bots'
|
||||
use_notice: false
|
||||
skip_join: false
|
||||
|
||||
env:
|
||||
- CXX=g++-4.8 NODE_ENV=test DB=memory
|
||||
- CXX=g++-4.8 NODE_ENV=test DB=mysql
|
||||
|
||||
before_install:
|
||||
- npm i -g npm@6
|
||||
- npm config set spin false
|
||||
|
||||
before_script:
|
||||
- "mysql -u root -NBe 'select version()'"
|
||||
- "mysql -u root -e 'DROP DATABASE IF EXISTS fxa_oauth;'"
|
||||
- "mysql -u root -e 'CREATE DATABASE fxa_oauth CHARACTER SET utf8 COLLATE utf8_unicode_ci;'"
|
||||
|
||||
script:
|
||||
- npm run outdated
|
||||
- npm test
|
||||
# HACK: ignore npm audit errors for now until we get them all fixed
|
||||
- npm audit || true
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,81 +0,0 @@
|
|||
# Contributing
|
||||
|
||||
Anyone is welcome to help with Firefox Accounts. Feel free to get in touch with other community members on IRC, the
|
||||
mailing list or through issues here on GitHub.
|
||||
|
||||
- IRC: `#fxa` on `irc.mozilla.org`
|
||||
- Mailing list: <https://mail.mozilla.org/listinfo/dev-fxacct>
|
||||
- and of course, [the issues list](https://github.com/mozilla/fxa-oauth-server/issues)
|
||||
|
||||
## Bug Reports ##
|
||||
|
||||
You can file issues here on GitHub. Please try to include as much information as you can and under what conditions
|
||||
you saw the issue.
|
||||
|
||||
## Sending Pull Requests ##
|
||||
|
||||
Patches should be submitted as pull requests (PR).
|
||||
|
||||
Before submitting a PR:
|
||||
- Your code must run and pass all the automated tests before you submit your PR for review. "Work in progress" pull requests are allowed to be submitted, but should be clearly labeled as such and should not be merged until all tests pass and the code has been reviewed.
|
||||
- Run `grunt eslint` to make sure your code passes linting.
|
||||
- Run `npm test` to make sure all tests still pass.
|
||||
- Your patch should include new tests that cover your changes. It is your and your reviewer's responsibility to ensure your patch includes adequate tests.
|
||||
|
||||
When submitting a PR:
|
||||
- You agree to license your code under the project's open source license ([MPL 2.0](/LICENSE)).
|
||||
- Base your branch off the current `master` (see below for an example workflow).
|
||||
- Add both your code and new tests if relevant.
|
||||
- Run `grunt eslint` and `npm test` to make sure your code passes linting and tests.
|
||||
- Please do not include merge commits in pull requests; include only commits with the new relevant code.
|
||||
- Your commit message must follow the [commit guidelines](https://github.com/mozilla/fxa/blob/master/CONTRIBUTING.md#git-commit-guidelines).
|
||||
|
||||
See the main [README.md](/README.md) for information on prerequisites, installing, running and testing.
|
||||
|
||||
## Code Review ##
|
||||
|
||||
This project is production Mozilla code and subject to our [engineering practices and quality standards](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Committing_Rules_and_Responsibilities). Every patch must be peer reviewed. This project is part of the [Firefox Accounts module](https://wiki.mozilla.org/Modules/Other#Firefox_Accounts), and your patch must be reviewed by one of the listed module owners or peers.
|
||||
|
||||
## Example Workflow ##
|
||||
|
||||
This is an example workflow to make it easier to submit Pull Requests. Imagine your username is `user1`:
|
||||
|
||||
1. Fork this repository via the GitHub interface
|
||||
|
||||
2. The clone the upstream (as origin) and add your own repo as a remote:
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/mozilla/fxa-oauth-server.git
|
||||
$ cd fxa-oauth-server
|
||||
$ git remote add user1 git@github.com:user1/fxa-oauth-server.git
|
||||
```
|
||||
|
||||
3. Create a branch for your fix/feature and make sure it's your currently checked-out branch:
|
||||
|
||||
```sh
|
||||
$ git checkout -b add-new-feature
|
||||
```
|
||||
|
||||
4. Add/fix code, add tests then commit and push this branch to your repo:
|
||||
|
||||
```sh
|
||||
$ git add <files...>
|
||||
$ git commit
|
||||
$ git push user1 add-new-feature
|
||||
```
|
||||
|
||||
5. From the GitHub interface for your repo, click the `Review Changes and Pull Request` which appears next to your new branch.
|
||||
|
||||
6. Click `Send pull request`.
|
||||
|
||||
### Keeping up to Date ###
|
||||
|
||||
The main reason for creating a new branch for each feature or fix is so that you can track master correctly. If you need
|
||||
to fetch the latest code for a new fix, try the following:
|
||||
|
||||
```sh
|
||||
$ git checkout master
|
||||
$ git pull
|
||||
```
|
||||
|
||||
Now you're ready to branch again for your new feature (from step 3 above).
|
|
@ -1,46 +0,0 @@
|
|||
FROM node:8-alpine AS builder
|
||||
|
||||
RUN npm install -g npm@6 && rm -rf ~app/.npm /tmp/*
|
||||
|
||||
RUN apk add --no-cache git && \
|
||||
apk add --repository http://dl-cdn.alpinelinux.org/alpine/v3.5/community/ --no-cache --virtual .build-deps git python make g++
|
||||
|
||||
RUN addgroup -g 10001 app && \
|
||||
adduser -D -G app -h /app -u 10001 app
|
||||
WORKDIR /app
|
||||
|
||||
# S3 bucket in Cloud Services prod IAM
|
||||
ADD https://s3.amazonaws.com/dumb-init-dist/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init
|
||||
RUN chmod +x /usr/local/bin/dumb-init
|
||||
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
|
||||
|
||||
USER app
|
||||
|
||||
COPY npm-shrinkwrap.json npm-shrinkwrap.json
|
||||
COPY package.json package.json
|
||||
COPY scripts/gen_keys.js scripts/gen_keys.js
|
||||
|
||||
RUN npm install --production && rm -rf ~app/.npm /tmp/*
|
||||
|
||||
COPY . /app
|
||||
|
||||
|
||||
# Build final image by copying from builder
|
||||
FROM node:8-alpine
|
||||
|
||||
RUN npm install -g npm@6 && rm -rf ~app/.npm /tmp/*
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
RUN addgroup -g 10001 app && \
|
||||
adduser -D -G app -h /app -u 10001 app
|
||||
WORKDIR /app
|
||||
|
||||
# S3 bucket in Cloud Services prod IAM
|
||||
ADD https://s3.amazonaws.com/dumb-init-dist/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init
|
||||
RUN chmod +x /usr/local/bin/dumb-init
|
||||
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
|
||||
|
||||
USER app
|
||||
|
||||
COPY --from=builder --chown=app /app/ /app/
|
|
@ -1,5 +0,0 @@
|
|||
FROM fxa-oauth-server:build
|
||||
USER root
|
||||
RUN apk add --repository http://dl-cdn.alpinelinux.org/alpine/v3.5/community/ --no-cache --virtual .build-deps git python make g++
|
||||
USER app
|
||||
RUN npm install
|
|
@ -1,25 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
|
||||
// show elapsed time at the end
|
||||
require('time-grunt')(grunt);
|
||||
// load all grunt tasks
|
||||
require('load-grunt-tasks')(grunt);
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('./package.json'),
|
||||
// .js files for ESLint, JSHint, JSCS, etc.
|
||||
mainJsFiles: '{,lib/**/,scripts/**/,test/**/,tasks/**/,bin/**/}*.js'
|
||||
});
|
||||
|
||||
grunt.loadTasks('grunttasks');
|
||||
|
||||
grunt.registerTask('default', [
|
||||
'lint',
|
||||
'copyright'
|
||||
]);
|
||||
};
|
|
@ -1,373 +0,0 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
|
@ -1,22 +0,0 @@
|
|||
# Firefox Accounts OAuth Server
|
||||
|
||||
[![Build Status](https://travis-ci.org/mozilla/fxa-oauth-server.svg?branch=master)](https://travis-ci.org/mozilla/fxa-oauth-server)
|
||||
[![CircleCI](https://circleci.com/gh/mozilla/fxa-oauth-server.svg?style=svg)](https://circleci.com/gh/mozilla/fxa-oauth-server)
|
||||
|
||||
Implementation of OAuth for use by Firefox Accounts
|
||||
|
||||
[API docs](./docs/api.md)
|
||||
|
||||
[Design document](https://github.com/mozilla/fxa-oauth-server/wiki/oauth-design)
|
||||
|
||||
[MDN docs](https://developer.mozilla.org/en-US/Firefox_Accounts)
|
||||
|
||||
## Quick Start
|
||||
|
||||
Clone the repository, run `npm install` and `npm start`.
|
||||
|
||||
To get a full development setup running use [fxa-local-dev](https://github.com/mozilla/fxa-local-dev). fxa-local-dev is the preferred way of contributing to Firefox Accounts.
|
||||
|
||||
## License
|
||||
|
||||
MPL 2.0
|
|
@ -1,23 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const config = require('../lib/config').getProperties();
|
||||
const db = require('../lib/db');
|
||||
const logger = require('../lib/logging')('bin.internal');
|
||||
const serverPromise = require('../lib/server/internal').create();
|
||||
|
||||
logger.debug('config', config);
|
||||
db.ping().done(function() {
|
||||
let server;
|
||||
|
||||
serverPromise.then((s) => {
|
||||
server = s;
|
||||
return server.start();
|
||||
}).then(() => {
|
||||
logger.info('listening', server.info.uri);
|
||||
});
|
||||
}, function(err) {
|
||||
logger.critical('db.ping', err);
|
||||
process.exit(1);
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* This is a command line tool that can be used to purge expired tokens
|
||||
* from the OAuth database. It requires you specify a pocket client id
|
||||
* before running. Currently, access tokens created from pocket should
|
||||
* not be deleted even if expired.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* node purge_expired_tokens.js --config dev --pocket-id dcdb5ae7add825d2 --token-count 10000 --delay-seconds 1
|
||||
*
|
||||
* or, for multiple pocket ids:
|
||||
*
|
||||
* node purge_expired_tokens.js --config dev --pocket-id dcdb5ae7add825d2,678f75ae1c0f8002 --token-count 10000 --delay-seconds 1
|
||||
*
|
||||
* */
|
||||
|
||||
const config = require('../lib/config');
|
||||
const package = require('../package.json');
|
||||
const program = require('commander');
|
||||
|
||||
// Don't bother updating the clients table.
|
||||
config.set('db.autoUpdateClients', false);
|
||||
|
||||
program
|
||||
.version(package.version)
|
||||
.option('-c, --config [config]', 'Configuration to use. Ex. dev')
|
||||
.option('-p, --pocket-id <pocketId>', 'Pocket Client Ids. These tokens will not be purged. (CSV)')
|
||||
.option('-t, --token-count <tokenCount>', 'Total number of tokens to delete.')
|
||||
.option('-d, --delay-seconds <delaySeconds>', 'Delay (seconds) between each deletion round. (Default: 1 second)')
|
||||
.option('-I, --by-id', 'Delete tokens by selecting, then deleting by primary id (Default: false)')
|
||||
.option('-D, --delete-batch-size <deleteBatchSize>', 'Number of tokens to delete in each deletion round. (Default: 200)')
|
||||
.parse(process.argv);
|
||||
|
||||
if (! program.config) {
|
||||
program.config = 'dev';
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = program.config;
|
||||
|
||||
const db = require('../lib/db');
|
||||
const logger = require('../lib/logging')('bin.purge_expired_tokens');
|
||||
|
||||
if (! program.pocketId) {
|
||||
logger.error('invalid', { message: 'Required pocket client id!' });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const numberOfTokens = parseInt(program.tokenCount) || 200;
|
||||
const delaySeconds = Number(program.delaySeconds) || 1; // Default 1 seconds
|
||||
const deleteBatchSize = Number(program.deleteBatchSize) || 200; // Default 200
|
||||
// There may be more than one pocketId, so treat this as a comma-separated list.
|
||||
const ignorePocketClientId = program.pocketId.toLowerCase().split(/\s*,\s*/g);
|
||||
|
||||
db.ping().done(() => {
|
||||
// Only mysql impl supports token deletion at the moment
|
||||
if (! db.purgeExpiredTokens) {
|
||||
const message = ('Unable to purge expired tokens, only available ' +
|
||||
'when using config with mysql database.');
|
||||
logger.info('skipping', { message: message });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('deleting', {
|
||||
numberOfTokens: numberOfTokens,
|
||||
delaySeconds: delaySeconds,
|
||||
deleteBatchSize: deleteBatchSize,
|
||||
ignorePocketClientId: ignorePocketClientId
|
||||
});
|
||||
|
||||
// To reduce the risk of deleting pocket tokens, ensure that the pocket-id
|
||||
// passed in belongs to a client.
|
||||
const purgeMethod = program.byId ? db.purgeExpiredTokensById : db.purgeExpiredTokens;
|
||||
return purgeMethod(numberOfTokens,
|
||||
delaySeconds,
|
||||
ignorePocketClientId,
|
||||
deleteBatchSize)
|
||||
.then(() => {
|
||||
logger.info('completed');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('error', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}, (err) => {
|
||||
logger.critical('db.ping', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
logger.error('error', err);
|
||||
process.exit(2);
|
||||
});
|
|
@ -1,30 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const config = require('../lib/config').getProperties();
|
||||
const db = require('../lib/db');
|
||||
const logger = require('../lib/logging')('bin.server');
|
||||
const serverPromise = require('../lib/server').create();
|
||||
const events = require('../lib/events');
|
||||
|
||||
logger.debug('config', config);
|
||||
db.ping().done(function() {
|
||||
let server;
|
||||
|
||||
serverPromise.then((s) => {
|
||||
server = s;
|
||||
return server.start();
|
||||
}).then(() => {
|
||||
logger.info('listening', server.info.uri);
|
||||
events.start();
|
||||
});
|
||||
|
||||
}, function(err) {
|
||||
logger.critical('db.ping', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function() {
|
||||
process.exit(2);
|
||||
});
|
|
@ -1,160 +0,0 @@
|
|||
{
|
||||
"browserid": {
|
||||
"issuer": "127.0.0.1:9000",
|
||||
"verificationUrl": "http://127.0.0.1:5050/v2"
|
||||
},
|
||||
"contentUrl": "http://127.0.0.1:3030/oauth/",
|
||||
"clients": [
|
||||
{
|
||||
"id": "dcdb5ae7add825d2",
|
||||
"hashedSecret": "289a885946ee316844d9ffd0d725ee714901548a1e6507f1a40fb3c2ae0c99f1",
|
||||
"name": "123Done",
|
||||
"imageUri": "https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png",
|
||||
"redirectUri": "http://127.0.0.1:8080/api/oauth",
|
||||
"trusted": true,
|
||||
"canGrant": false
|
||||
},
|
||||
{
|
||||
"id": "38a6b9b3a65a1871",
|
||||
"hashedSecret": "289a885946ee316844d9ffd0d725ee714901548a1e6507f1a40fb3c2ae0c99f1",
|
||||
"name": "123Done PKCE",
|
||||
"imageUri": "https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png",
|
||||
"redirectUri": "http://127.0.0.1:8080/?oauth_pkce_redirect=1",
|
||||
"trusted": true,
|
||||
"canGrant": false,
|
||||
"publicClient": true
|
||||
},
|
||||
{
|
||||
"id": "22d74070a481bc73",
|
||||
"name": "Test Client iOS",
|
||||
"hashedSecret": "88716ed2927c96cdc0fb7efe57d5f124fb4161066c1ff7f4263069822256ec66",
|
||||
"redirectUri": "com.mozilla.sandvich:/oauth2redirect/fxa-provider",
|
||||
"imageUri": "",
|
||||
"publicClient": true,
|
||||
"canGrant": false,
|
||||
"termsUri": "",
|
||||
"privacyUri": "",
|
||||
"trusted": true,
|
||||
"allowedScopes": "https://identity.mozilla.com/apps/oldsync"
|
||||
},
|
||||
{
|
||||
"id": "325b4083e32fe8e7",
|
||||
"hashedSecret": "ded3c396f28123f3fe6b152784e8eab7357c6806cb5175805602a2cd67f85080",
|
||||
"name": "321Done Untrusted",
|
||||
"imageUri": "https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png",
|
||||
"redirectUri": "http://127.0.0.1:10139/api/oauth",
|
||||
"trusted": false,
|
||||
"canGrant": false
|
||||
},
|
||||
{
|
||||
"id": "7f368c6886429f19",
|
||||
"name": "Firefox Notes Android Dev",
|
||||
"hashedSecret": "9c716ed2927c96cdc0fb7efe57d5f124fb4161066c1ff7f4263069822256ec3f",
|
||||
"redirectUri": "https://mozilla.github.io/notes/fxa/android-redirect.html",
|
||||
"imageUri": "",
|
||||
"canGrant": false,
|
||||
"termsUri": "",
|
||||
"privacyUri": "",
|
||||
"trusted": true,
|
||||
"allowedScopes": "https://identity.mozilla.com/apps/notes",
|
||||
"publicClient": true
|
||||
},
|
||||
{
|
||||
"id": "c6d74070a481bc10",
|
||||
"name": "Firefox Notes Dev",
|
||||
"hashedSecret": "9c716ed2927c96cdc0fb7efe57d5f124fb4161066c1ff7f4263069822256ec3f",
|
||||
"redirectUri": "https://dee85c67bd72f3de1f0a0fb62a8fe9b9b1a166d7.extensions.allizom.org/",
|
||||
"imageUri": "",
|
||||
"canGrant": false,
|
||||
"termsUri": "",
|
||||
"privacyUri": "",
|
||||
"trusted": true,
|
||||
"allowedScopes": "https://identity.mozilla.com/apps/notes",
|
||||
"publicClient": true
|
||||
},
|
||||
{
|
||||
"id": "98e6508e88680e1a",
|
||||
"hashedSecret": "ba5cfb370fd782f7eae1807443ab816288c101a54c0d80a09063273c86d3c435",
|
||||
"name": "Firefox Accounts Settings",
|
||||
"imageUri": "https://example2.domain/logo",
|
||||
"redirectUri": "https://example2.domain/return?foo=bar",
|
||||
"trusted": true,
|
||||
"canGrant": true
|
||||
},
|
||||
{
|
||||
"name": "FxA OAuth Console",
|
||||
"redirectUri": "http://127.0.0.1:10137/oauth/redirect",
|
||||
"imageUri": "http://127.0.0.1:10137/assets/firefox.png",
|
||||
"id": "24bdbfa45cd300c5",
|
||||
"hashedSecret": "dfe56d5c816d6b7493618f6a1567cfed4aa9c25f85d59c6804631c48774ba545",
|
||||
"trusted": true,
|
||||
"canGrant": false
|
||||
},
|
||||
{
|
||||
"name": "Firefox Hello DEV",
|
||||
"redirectUri": "urn:ietf:wg:oauth:2.0:fx:webchannel",
|
||||
"imageUri": "https://example2.domain/return?foo=bar",
|
||||
"id": "263ceaa5546dce83",
|
||||
"hashedSecret": "3c32099123471ffb80a7553558e6a6e8589da2235a4e081fe9d76648aa25d050",
|
||||
"trusted": true,
|
||||
"canGrant": false
|
||||
},
|
||||
{
|
||||
"name": "Firefox",
|
||||
"id": "5882386c6d801776",
|
||||
"hashedSecret": "71b5283536f1f1c331eca2f75c58a5947d7a7ac54164eadb4b33a889afe89fbf",
|
||||
"imageUri": "",
|
||||
"redirectUri": "urn:ietf:wg:oauth:2.0:oob",
|
||||
"trusted": true,
|
||||
"canGrant": true
|
||||
},
|
||||
{
|
||||
"name": "Fennec",
|
||||
"id": "3332a18d142636cb",
|
||||
"hashedSecret": "99ee06fa07919c5208694d34d761fa95ee5a0bbbaad3f3ebaa6042b04a6bdec1",
|
||||
"imageUri": "",
|
||||
"redirectUri": "urn:ietf:wg:oauth:2.0:oob",
|
||||
"trusted": true,
|
||||
"canGrant": true
|
||||
},
|
||||
{
|
||||
"name": "Firefox OS",
|
||||
"id": "d0eea24a1d613eeb",
|
||||
"hashedSecret": "5a1f1deb84752118a8b8020b7f2c6f9aea76a0e968bead2478ebc309704e29ee",
|
||||
"imageUri": "",
|
||||
"redirectUri": "urn:ietf:wg:oauth:2.0:oob",
|
||||
"trusted": true,
|
||||
"canGrant": true
|
||||
},
|
||||
{
|
||||
"name": "Firefox Accounts",
|
||||
"id": "ea3ca969f8c6bb0d",
|
||||
"hashedSecret": "744559ea3d0f69eb5185cbd5b176a38e09d013c6459dbb3cbc25b4c5b165d33f",
|
||||
"imageUri": "",
|
||||
"redirectUri": "urn:ietf:wg:oauth:2.0:oob",
|
||||
"trusted": true,
|
||||
"canGrant": true
|
||||
}
|
||||
],
|
||||
"localRedirects": true,
|
||||
"logging": {
|
||||
"level": "ALL",
|
||||
"fmt": "pretty"
|
||||
},
|
||||
"openid": {
|
||||
"issuer": "http://127.0.0.1:3030",
|
||||
"keyFile": "../config/key.json",
|
||||
"key": {}
|
||||
},
|
||||
"allowHttpRedirects": true,
|
||||
"scopes": [
|
||||
{
|
||||
"scope": "https://identity.mozilla.com/apps/notes",
|
||||
"hasScopedKeys": true
|
||||
},
|
||||
{
|
||||
"scope": "https://identity.mozilla.com/apps/oldsync",
|
||||
"hasScopedKeys": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
{
|
||||
"clients": [
|
||||
{
|
||||
"id": "dcdb5ae7add825d2",
|
||||
"hashedSecret": "289a885946ee316844d9ffd0d725ee714901548a1e6507f1a40fb3c2ae0c99f1",
|
||||
"hashedSecretPrevious": "0726282857047586fb4edc335b5492ef1e4a0d95d3f1114627bb89b4e57cf6e1",
|
||||
"name": "Mocha",
|
||||
"imageUri": "https://example.domain/logo",
|
||||
"redirectUri": "https://example.domain/return?foo=bar",
|
||||
"trusted": true,
|
||||
"canGrant": false
|
||||
},
|
||||
{
|
||||
"id": "98e6508e88680e1a",
|
||||
"hashedSecret": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"name": "Admin",
|
||||
"imageUri": "https://example2.domain/logo",
|
||||
"redirectUri": "https://example2.domain/redirect",
|
||||
"trusted": true,
|
||||
"canGrant": true,
|
||||
"publicClient": true
|
||||
},
|
||||
{
|
||||
"id": "98e6508e88680e1b",
|
||||
"hashedSecret": "ba5cfb370fd782f7eae1807443ab816288c101a54c0d80a09063273c86d3c435",
|
||||
"name": "URN",
|
||||
"imageUri": "https://example2.domain/logo",
|
||||
"redirectUri": "urn:ietf:wg:oauth:2.0:fx:webchannel",
|
||||
"trusted": true,
|
||||
"canGrant": false
|
||||
},
|
||||
{
|
||||
"name": "NoRedirectUri",
|
||||
"id": "ea3ca969f8c6bb0d",
|
||||
"hashedSecret": "d962cdf34a33ab26f7a6b900d0e1028f182d8e4811cb9b5ac4f20275525c8f54",
|
||||
"imageUri": "",
|
||||
"redirectUri": "",
|
||||
"trusted": true,
|
||||
"canGrant": false
|
||||
},
|
||||
{
|
||||
"name": "Untrusted",
|
||||
"id": "ea3ca969f8c6bb0e",
|
||||
"hashedSecret": "ec62e3281e3b56e702fe7e82ca7b1fa59d6c2a6766d6d28cccbf8bfa8d5fc8a8",
|
||||
"imageUri": "",
|
||||
"redirectUri": "https://example.domain/return?foo=bar",
|
||||
"trusted": false,
|
||||
"canGrant": false
|
||||
},
|
||||
{
|
||||
"id": "38a6b9b3a65a1871",
|
||||
"hashedSecret": "289a885946ee316844d9ffd0d725ee714901548a1e6507f1a40fb3c2ae0c99f1",
|
||||
"name": "Public Client PKCE",
|
||||
"imageUri": "https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png",
|
||||
"redirectUri": "https://example.domain/return?foo=bar",
|
||||
"trusted": true,
|
||||
"allowedScopes": "kv",
|
||||
"canGrant": false,
|
||||
"publicClient": true
|
||||
},
|
||||
{
|
||||
"id": "aaa6b9b3a65a1871",
|
||||
"hashedSecret": "289a885946ee316844d9ffd0d725ee714901548a1e6507f1a40fb3c2ae0c99f1",
|
||||
"name": "Scoped Key Client",
|
||||
"imageUri": "https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png",
|
||||
"redirectUri": "https://example.domain/return?foo=bar",
|
||||
"trusted": true,
|
||||
"allowedScopes": "https://identity.mozilla.com/apps/sample-scope-can-scope-key https://identity.mozilla.com/apps/sample-scope kv https://identity.mozilla.com/apps/another-can-scope-key",
|
||||
"canGrant": false,
|
||||
"publicClient": false
|
||||
}
|
||||
],
|
||||
"logging": {
|
||||
"level": "error",
|
||||
"fmt": "pretty"
|
||||
},
|
||||
"openid": {
|
||||
"keyFile": "../config/key.json",
|
||||
"oldKeyFile": "../config/oldKey.json",
|
||||
"key": {},
|
||||
"oldKey": {}
|
||||
},
|
||||
"serviceClients": [
|
||||
{
|
||||
"id": "d23dbf62b82eb04e",
|
||||
"name": "Test Service Client",
|
||||
"scope": "profile",
|
||||
"jku": "http://127.0.0.1:9019/.well-known/public-keys"
|
||||
}
|
||||
],
|
||||
"allowHttpRedirects": true,
|
||||
"scopes": [
|
||||
{
|
||||
"scope": "https://identity.mozilla.com/apps/sample-scope",
|
||||
"hasScopedKeys": false
|
||||
},
|
||||
{
|
||||
"scope": "https://identity.mozilla.com/apps/sample-scope-can-scope-key",
|
||||
"hasScopedKeys": true
|
||||
},
|
||||
{
|
||||
"scope": "https://identity.mozilla.com/apps/another-can-scope-key",
|
||||
"hasScopedKeys": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,669 +0,0 @@
|
|||
# Firefox Accounts OAuth Server API
|
||||
|
||||
## Overview
|
||||
|
||||
### URL Structure
|
||||
|
||||
```
|
||||
https://<server-url>/v1/<api-endpoint>
|
||||
```
|
||||
|
||||
Note that:
|
||||
|
||||
- All API access must be over HTTPS.
|
||||
- The URL embeds a version identifier "v1"; future revisions of this API may introduce new version numbers.
|
||||
- The base URL of the server may be configured on a per-client basis.
|
||||
|
||||
### Errors
|
||||
|
||||
Invalid requests will return 4XX responses. Internal failures will return 5XX. Both will include JSON responses describing the error.
|
||||
|
||||
**Example error:**
|
||||
|
||||
```js
|
||||
{
|
||||
"code": 400, // matches the HTTP status code
|
||||
"errno": 101, // stable application-level error number
|
||||
"error": "Bad Request", // string description of error type
|
||||
"message": "Unknown client"
|
||||
}
|
||||
```
|
||||
|
||||
The currently-defined error responses are:
|
||||
|
||||
| status code | errno | description |
|
||||
|:-----------:|:-----:|-------------|
|
||||
| 400 | 101 | unknown client id |
|
||||
| 400 | 102 | incorrect client secret |
|
||||
| 400 | 103 | `redirect_uri` doesn't match registered value |
|
||||
| 401 | 104 | invalid fxa assertion |
|
||||
| 400 | 105 | unknown code |
|
||||
| 400 | 106 | incorrect code |
|
||||
| 400 | 107 | expired code |
|
||||
| 400 | 108 | invalid token |
|
||||
| 400 | 109 | invalid request parameter |
|
||||
| 400 | 110 | invalid response_type |
|
||||
| 401 | 111 | unauthorized |
|
||||
| 403 | 112 | forbidden |
|
||||
| 415 | 113 | invalid content type |
|
||||
| 400 | 114 | invalid scopes |
|
||||
| 400 | 115 | expired token |
|
||||
| 500 | 999 | internal server error |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
|
||||
- [GET /v1/authorization][redirect]
|
||||
- [GET /v1/jwks][jwks]
|
||||
- [POST /v1/authorization][authorization]
|
||||
- [POST /v1/token][token]
|
||||
- [POST /v1/destroy][delete]
|
||||
- Clients
|
||||
- [GET /v1/client/:id][client]
|
||||
- [GET /v1/clients][clients]
|
||||
- [POST /v1/client][register]
|
||||
- [POST /v1/client/:id][client-update]
|
||||
- [DELETE /v1/client/:id][client-delete]
|
||||
- Developers
|
||||
- [POST /v1/developer/activate][developer-activate]
|
||||
- [POST /v1/verify][verify]
|
||||
- [POST /v1/key-data][key-data]
|
||||
- [GET /v1/client-tokens][client-tokens]
|
||||
- [DELETE /v1/client-tokens/:id][client-tokens-delete]
|
||||
|
||||
### GET /v1/client/:id
|
||||
|
||||
This endpoint is for the fxa-content-server to retrieve information
|
||||
about a client to show in its user interface.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- `id`: The `client_id` of a client asking for permission.
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -v "https://oauth.accounts.firefox.com/v1/client/5901bd09376fadaa"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid 200 response will be a JSON blob with the following properties:
|
||||
|
||||
- `name`: A string name of the client.
|
||||
- `image_uri`: A url to a logo or image that represents the client.
|
||||
- `redirect_uri`: The url registered to redirect to after successful oauth.
|
||||
- `trusted`: Whether the client is a trusted internal application.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Where's My Fox",
|
||||
"image_uri": "https://mozilla.org/firefox.png",
|
||||
"redirect_uri": "https://wheres.my.firefox.com/oauth",
|
||||
"trusted": true
|
||||
}
|
||||
```
|
||||
|
||||
### GET /v1/clients
|
||||
|
||||
Get a list of all registered clients.
|
||||
|
||||
**Required scope:** `oauth`
|
||||
|
||||
#### Request
|
||||
|
||||
**Example:**
|
||||
|
||||
|
||||
```sh
|
||||
curl -v \
|
||||
-H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \
|
||||
"https://oauth.accounts.firefox.com/v1/clients"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid 200 response will be a JSON object with a property of `clients`,
|
||||
which contains an array of client objects.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"clients": [
|
||||
{
|
||||
"id": "5901bd09376fadaa",
|
||||
"name": "Example",
|
||||
"redirect_uri": "https://ex.am.ple/path",
|
||||
"image_uri": "https://ex.am.ple/logo.png",
|
||||
"can_grant": false,
|
||||
"trusted": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /v1/client
|
||||
|
||||
Register a new client (FxA relier).
|
||||
|
||||
**Required scope:** `oauth`
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- `name`: The name of the client.
|
||||
- `redirect_uri`: The URI to redirect to after logging in.
|
||||
- `image_uri`: A URI to an image to show to a user when logging in.
|
||||
- `trusted`: Whether the client is a trusted internal application.
|
||||
- `can_grant`: A client needs permission to get implicit grants.
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -v \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \
|
||||
"https://oauth.accounts.firefox.com/v1/client" \
|
||||
-d '{
|
||||
"name": "Example",
|
||||
"redirect_uri": "https://ex.am.ple/path",
|
||||
"image_uri": "https://ex.am.ple/logo.png",
|
||||
"trusted": false,
|
||||
"can_grant": false
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid 201 response will be a JSON blob with the following properties:
|
||||
|
||||
- `client_id`: The generated id for this client.
|
||||
- `client_secret`: The generated secret for this client. *NOTE: This is
|
||||
the only time you can get the secret, because we only keep a hashed
|
||||
version.*
|
||||
- `name`: A string name of the client.
|
||||
- `image_uri`: A url to a logo or image that represents the client.
|
||||
- `redirect_uri`: The url registered to redirect to after successful oauth.
|
||||
- `can_grant`: If the client can get implicit grants.
|
||||
- `trusted`: Whether the client is a trusted internal application.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "5901bd09376fadaa",
|
||||
"client_secret": "4ab433e31ef3a7cf7c20590f047987922b5c9ceb1faff56f0f8164df053dd94c",
|
||||
"name": "Example",
|
||||
"redirect_uri": "https://ex.am.ple/path",
|
||||
"image_uri": "https://ex.am.ple/logo.png",
|
||||
"can_grant": false,
|
||||
"trusted": false
|
||||
}
|
||||
```
|
||||
|
||||
### POST /v1/client/:id
|
||||
|
||||
Update the details of a client. Any parameter not included in the
|
||||
request will stay unchanged.
|
||||
|
||||
**Required scope:** `oauth`
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- `name`: The name of the client.
|
||||
- `redirect_uri`: The URI to redirect to after logging in.
|
||||
- `image_uri`: A URI to an image to show to a user when logging in.
|
||||
- `trusted`: Whether the client is a trusted internal application.
|
||||
- `can_grant`: A client needs permission to get implicit grants.
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -v \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \
|
||||
"https://oauth.accounts.firefox.com/v1/client/5901bd09376fadaa" \
|
||||
-d '{
|
||||
"name": "Example2",
|
||||
"redirect_uri": "https://ex.am.ple/path/2",
|
||||
"image_uri": "https://ex.am.ple/logo2.png",
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid response will have a 200 status code and empty object `{}`.
|
||||
|
||||
### DELETE /v1/client/:id
|
||||
|
||||
Delete a client. It will be no more. Zilch. Nada. Nuked from orbit.
|
||||
|
||||
**Required scope:** `oauth`
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -v \
|
||||
-X DELETE \
|
||||
-H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \
|
||||
"https://oauth.accounts.firefox.com/v1/client/5901bd09376fadaa"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid response will have a 204 response code and an empty body.
|
||||
|
||||
### POST /v1/developer/activate
|
||||
|
||||
Register an oauth developer.
|
||||
|
||||
**Required scope:** `oauth`
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- None
|
||||
|
||||
#### Response
|
||||
|
||||
A valid response will have a 200 status code and a developer object:
|
||||
```
|
||||
{"developerId":"f5b176ab5be5928d01d4bb0a6c182994","email":"d91c30a8@mozilla.com","createdAt":"2015-03-23T01:22:59.000Z"}
|
||||
```
|
||||
|
||||
### GET /v1/authorization
|
||||
|
||||
This endpoint starts the OAuth flow. A client redirects the user agent
|
||||
to this url. This endpoint will then redirect to the appropriate
|
||||
content-server page.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- `client_id`: The id returned from client registration.
|
||||
- `state`: A value that will be returned to the client as-is upon redirection, so that clients can verify the redirect is authentic.
|
||||
- `redirect_uri`: Optional. If supplied, a string URL of where to redirect afterwards. Must match URL from registration.
|
||||
- `scope`: Optional. A space-separated list of scopes that the user has authorized. This could be pruned by the user at the confirmation dialog. If this includes the scope `openid`, this will be an OpenID Connect authentication request.
|
||||
- `access_type`: Optional. If provided, should be `online` or `offline`. `offline` will result in a refresh_token being provided, so that the access_token can be refreshed after it expires.
|
||||
- `action`: Optional. If provided, should be `email`, `signup`, `signin`, or `force_auth`. Send to improve the user experience.
|
||||
- If unspecified then Firefox Accounts will try choose intelligently between `signin` and `signup` based on the user's browser state.
|
||||
- `email` triggers the email-first flow, which uses the email address to determine whether to display signup or signin. This is becoming the **preferred** action and is slowly replacing `signin` and `signup`.
|
||||
- `signin` triggers the signin flow. (will become depricated and replaced by `email`)
|
||||
- `signup` triggers the signup flow. (will become depricated and replaced by `email`)
|
||||
- `force_auth` requires the user to sign in using the address specified in `email`.
|
||||
- `email`: Optional if `action` is `email`, `signup` or `signin`. Required if `action`
|
||||
is `force_auth`.
|
||||
- if `action` is `email`, the email address will be used to determine whether to display the signup or signin form, but the user is free to change it.
|
||||
- If `action` is `signup` or `signin`, the email address will be pre-filled into the account form, but the user is free to change it.
|
||||
- If `action` is `signin`, the literal string `blank` will force the user to enter an email address and the last signed in email address will be ignored.
|
||||
- If `action` is `signin` and no email address is specified, the last
|
||||
signed in email address will be used as the default.
|
||||
- If `action` is `force_auth`, the user is unable to modify the email
|
||||
address and is unable to sign up if the address is not registered.
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -v "https://oauth.accounts.firefox.com/v1/authorization?client_id=5901bd09376fadaa&state=1234&scope=profile:email&action=signup"
|
||||
```
|
||||
|
||||
### POST /v1/authorization
|
||||
|
||||
This endpoint should be used by the fxa-content-server, requesting that
|
||||
we supply a short-lived code (currently 15 minutes) that will be sent
|
||||
back to the client. This code will be traded for a token at the
|
||||
[token][] endpoint.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- `client_id`: The id returned from client registration.
|
||||
- `assertion`: A FxA assertion for the signed-in user.
|
||||
- `state`: A value that will be returned to the client as-is upon redirection, so that clients can verify the redirect is authentic.
|
||||
- `response_type`: Optional. If supplied, must be either `code` or `token`. `code` is the default. `token` means the implicit grant is desired, and requires that the client have special permission to do so.
|
||||
- `ttl`: Optional if `response_type=token`, forbidden if `response_type=code`. Indicates the requested lifespan in seconds for the implicit grant token. The value is subject to an internal maximum limit, so clients must check the `expires_in` result property for the actual TTL.
|
||||
- `redirect_uri`: Optional. If supplied, a string URL of where to redirect afterwards. Must match URL from registration.
|
||||
- `scope`: Optional. A string-separated list of scopes that the user has authorized. This could be pruned by the user at the confirmation dialog.
|
||||
- `access_type`: Optional. A value of `offline` will generate a refresh token along with the access token.
|
||||
- `code_challenge_method`: Required if using [PKCE](pkce.md). Must be `S256`, no other value is accepted.
|
||||
- `code_challenge`: Required if using [PKCE](pkce.md). A minimum length of 43 characters and a maximum length of 128 characters string, encoded as `BASE64URL`.
|
||||
- `keys_jwe`: Optional. A JWE bundle to be returned to the client when it redeems the authorization code.
|
||||
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -v \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://oauth.accounts.firefox.com/v1/authorization" \
|
||||
-d '{
|
||||
"client_id": "5901bd09376fadaa",
|
||||
"assertion": "<assertion>",
|
||||
"state": "1234",
|
||||
"scope": "profile:email"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid request will return a 200 response, with JSON containing the `redirect` to follow. It will include the following query parameters:
|
||||
|
||||
- `code`: A string that the client will trade with the [token][] endpoint. Codes have a configurable expiration value, default is 15 minutes. Codes are single use only.
|
||||
- `state`: The same value as was passed as a request parameter.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"redirect": "https://example.domain/path?foo=bar&code=4ab433e31ef3a7cf7c20590f047987922b5c9ceb1faff56f0f8164df053dd94c&state=1234"
|
||||
}
|
||||
```
|
||||
|
||||
##### Implicit Grant
|
||||
|
||||
If requesting an implicit grant (token), the response will match the
|
||||
[/v1/token][token] response.
|
||||
|
||||
|
||||
### POST /v1/token
|
||||
|
||||
After having received a [code][authorization], the client sends that code (most
|
||||
likely a server-side request) to this endpoint, to receive a
|
||||
longer-lived token that can be used to access attached services for a
|
||||
particular user.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- `ttl`: (optional) Seconds that this access_token should be valid.
|
||||
|
||||
The default and maximum value is 2 weeks.
|
||||
- `grant_type`: Either `authorization_code`, `refresh_token`, or `urn:ietf:params:oauth:grant-type:jwt-bearer`.
|
||||
- If `authorization_code`:
|
||||
- `client_id`: The id returned from client registration.
|
||||
- `client_secret`: The secret returned from client registration.
|
||||
- `code`: A string that was received from the [authorization][] endpoint.
|
||||
- If `refresh_token`:
|
||||
- `client_id`: The id returned from client registration.
|
||||
- `client_secret`: The secret returned from client registration.
|
||||
This must not be set if the client is a public (PKCE) client.
|
||||
- `refresh_token`: A string that received from the [token][]
|
||||
endpoint specifically as a refresh token.
|
||||
- `scope`: (optional) A subset of scopes provided to this
|
||||
refresh_token originally, to receive an access_token with less
|
||||
permissions.
|
||||
- If `urn:ietf:params:oauth:grant-type:jwt-bearer`:
|
||||
- `assertion`: A signed JWT assertion. See [Service
|
||||
Clients][] for more.
|
||||
- if client is type `publicClient:true` and `authorization_code`:
|
||||
- `code_verifier`: Required if using [PKCE](pkce.md).
|
||||
|
||||
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -v \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://oauth.accounts.firefox.com/v1/token" \
|
||||
-d '{
|
||||
"client_id": "5901bd09376fadaa",
|
||||
"client_secret": "20c6882ef864d75ad1587c38f9d733c80751d2cbc8614e30202dc3d1d25301ff",
|
||||
"ttl": 3600,
|
||||
"grant_type": "authorization_code",
|
||||
"code": "4ab433e31ef3a7cf7c20590f047987922b5c9ceb1faff56f0f8164df053dd94c"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid request will return a JSON response with these properties:
|
||||
|
||||
- `access_token`: A string that can be used for authorized requests to service providers.
|
||||
- `scope`: A string of space-separated permissions that this token has. May differ from requested scopes, since user can deny permissions.
|
||||
- `refresh_token`: (Optional) A refresh token to fetch a new access token when this one expires. Only will be present if `grant_type=authorization_code` and the original authorization request included `access_type=offline`.
|
||||
- `expires_in`: **Seconds** until this access token will no longer be valid.
|
||||
- `token_type`: A string representing the token type. Currently will always be "bearer".
|
||||
- `auth_at`: An integer giving the time at which the user authenticated to the Firefox Accounts server when generating this token, as a UTC unix timestamp (i.e. **seconds since epoch**).
|
||||
- `id_token`: (Optional) If the authorization was requested with `openid` scope, then this property will contain the OpenID Connect ID Token.
|
||||
- `keys_jwe`: (Optional) Returns the JWE bundle that if the authorization request had one.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0",
|
||||
"scope": "profile:email profile:avatar",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "58d59cc97c3ca183b3a87a65eec6f93d5be051415b53afbf8491cc4c45dbb0c6",
|
||||
"auth_at": 1422336613
|
||||
}
|
||||
```
|
||||
|
||||
### POST /v1/destroy
|
||||
|
||||
After a client is done using a token, the responsible thing to do is to
|
||||
destroy the token afterwards. A client can use this route to do so.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- `token` - The hex string token.
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -v \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://oauth.accounts.firefox.com/v1/destroy" \
|
||||
-d '{
|
||||
"token": "558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid request will return an empty response, with a 200 status code.
|
||||
|
||||
|
||||
### POST /v1/verify
|
||||
|
||||
Attached services can post tokens to this endpoint to learn about which
|
||||
user and scopes are permitted for the token.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- `token`: A token string received from a client
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -v \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://oauth.accounts.firefox.com/v1/verify" \
|
||||
-d '{
|
||||
"token": "558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid request will return JSON with these properties:
|
||||
|
||||
- `user`: The uid of the respective user.
|
||||
- `client_id`: The client_id of the respective client.
|
||||
- `scope`: An array of scopes allowed for this token.
|
||||
- `email`: **DEPRECATED** The email of the respective user.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"user": "5901bd09376fadaa076afacef5251b6a",
|
||||
"client_id": "45defeda038a1c92",
|
||||
"scope": ["profile:email", "profile:avatar"],
|
||||
"email": "foo@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### GET /v1/jwks
|
||||
|
||||
This endpoint returns the [JWKs](https://tools.ietf.org/html/rfc7517)
|
||||
that are used for signing OpenID Connect id tokens.
|
||||
|
||||
#### Request
|
||||
|
||||
```sh
|
||||
curl -v "https://oauth.accounts.firefox.com/v1/jwks"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid response will return JSON of the `keys`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"keys": [
|
||||
"alg": "RS256",
|
||||
"use": "sig",
|
||||
"kty": "RSA",
|
||||
"kid": "2015.12.02-1",
|
||||
"n":"xaQHsKpu1KSK-YEMoLzZS7Xxciy3esGrhrrqW_JBrq3IRmeGLaqlE80zcpIVnStyp9tbet2niYTemt8ug591YWO5Y-S0EgQyFTxnGjzNOvAL6Cd2iGie9QeSehfFLNyRPdQiadYw07fw-h5gweMpVJs8nTgS-Bcorlw9JQM6Il1cUpbP0Lt-F_5qrzlaOiTEAAb4JGOusVh0n-MZfKt7w0mikauMH5KfhflwQDn4YTzRkWJzlldXr1Cs0ZkYzOwS4Hcoku7vd6lqCUO0GgZvkuvCFqdVKzpa4CGboNdfIjcGVF4f1CTQaQ0ao51cwLzq1pgi5aWYhVH7lJcm6O_BQw",
|
||||
"e":"AQAC"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### POST /v1/post-keydata
|
||||
|
||||
This endpoint returns the required scoped key metadata.
|
||||
|
||||
#### Request
|
||||
|
||||
```sh
|
||||
curl -X POST \
|
||||
https://oauth.accounts.firefox.com/v1/key-data \
|
||||
-H 'cache-control: no-cache' \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{
|
||||
"client_id": "aaa6b9b3a65a1871",
|
||||
"assertion": "eyJhbGciOiJSUzI1NiJ9.eyJwdWJsaWMta2V5Ijp7Imt0eSI6IlJTQSIsIm4iOiJvWmdsNkpwM0Iwcm5BVXppNThrdS1iT0RvR3ZuUGNnWU1UdXQ1WkpyQkJiazBCdWU4VUlRQ0dnYVdrYU5Xb29INkktMUZ6SXU0VFpZYnNqWGJ1c2JRRlQxOGREUkN6VVRubFlXdVZXUzhoSWhKc3lhZHJwSHJOVkI1VndmSlRKZVgwTjFpczBXcU1qdUdOc2VMLXluYnFjOVhueElncFJaai05QnZqY2ZKYXNOUTNZdHR3VHZVaFJOLVFGNWgxQkY1MnA2QmdOTVBvWmQ5MC1EU0xydlpseXp6MEh0Q2tFZnNsc013czVkR0ExTlZ1dEwtcGVDeU50VTFzOEtFaDlzcGxXeF9lQlFybTlYQU1kYXp5ZWR6VUpJU1UyMjZmQzhEUHh5c0ZreXpCbjlDQnFDQUpTNjQzTGFydUVDaS1rMGhKOWFmM2JXTmJnWmpSNVJ2NXF4THciLCJlIjoiQVFBQiJ9LCJwcmluY2lwYWwiOnsiZW1haWwiOiIwNjIxMzM0YzIwNjRjNmYzNmJlOGFkOWE0N2M1NTliY2FwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9LCJpYXQiOjE1MDY5Njk2OTU0MzksImV4cCI6MTUwNjk2OTY5NjQzOSwiZnhhLXZlcmlmaWVkRW1haWwiOiIzMjM2NzJiZUBtb3ppbGxhLmNvbSIsImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.hFZd5zFheXOFrXKkJvw6Vpv2l7ctlxuBTvuh5f_jLPAjZoJ9ri-vaJjL_WYBFUvS2xHzfx3-ldxLddyTKwCDAJeB_NkOFL_WJSrMet9C7_Z1hH9HmydeXIT82xJmhrwzW-WOO4ibQvRbocEFiNujynKsg1gS8v0iiYjIX-0cXCrlkxkbVx_8EXJFKDDOGzK9v7Zq6D7gkhP-CHEaNYaTHMn65tLQtBS6snGdaXlxoGHMWmDL6STbnJzWa7sa4QwHf-AgT1rUkQQAUHNa_XLZ0FEzqiCPctMadlihiUZL2V6vxIDBS4mHUF4qj0FvIMJflivDnJVkRNijDuP-h-Lh_A~eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJvYXV0aC5meGEiLCJleHAiOjE1MDY5Njk2OTY0MzksImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.M5xyk3RffucgaavjbUm7Eqnt47hzeGbGa2VR3jnVEIlRHfz5S25Qf3ngejwee7XECvIywbaKWeijXFOwS-EkB-7qP1gl4oNJjPmbnCk7S1lgckLWvdMIU-HLGKjrN6Mw76__LzvAbsusSeGmsvTCIVuOJ49Xs3tC1fLyB_re0QNpCcS6AUnJ1KOxIMEM3Om7ysNO5F_AqcD3PwlEti5lbwSk8iP5TWL12C2Nkb_6Hxze_mA1NZNAHOips9bF2J7oy1hqGoMYj1XYZrsyjpPWEuZQATAPlKSjbh1hq-UtDeT7DlwEmIbIUd3JA8qh1MkHKGgavd4fIMap0IPmr9rs4A",
|
||||
"scope": "https://identity.mozilla.com/apps/sample-scope-can-scope-key"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid response will return JSON the scoped key information for every scope that has scoped keys:
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"https://identity.mozilla.com/apps/sample-scope-can-scope-key": {
|
||||
"identifier": "https://identity.mozilla.com/apps/sample-scope-can-scope-key",
|
||||
"keyRotationSecret": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"keyRotationTimestamp": 1506970363512
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /v1/client-tokens
|
||||
|
||||
This endpoint returns a list of all clients with active OAuth tokens for the user,
|
||||
including the the scopes granted to each client
|
||||
and the last time each client was active.
|
||||
It must be authenticated with an OAuth token bearing scope "clients:write".
|
||||
|
||||
#### Request
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -X GET \
|
||||
https://oauth.accounts.firefox.com/v1/client-tokens \
|
||||
-H 'cache-control: no-cache' \
|
||||
-H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid 200 response will be a JSON array
|
||||
where each item as the following properties:
|
||||
|
||||
- `id`: The hex id of the client.
|
||||
- `name`: The string name of the client.
|
||||
- `lastAccessTime`: Integer last-access time for the client.
|
||||
- `lastAccessTimeFormatted`: Localized string last-access time for the client.
|
||||
- `scope`: Sorted list of all scopes granted to the client.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "5901bd09376fadaa",
|
||||
"name": "Example",
|
||||
"lastAccessTime": 1528334748000,
|
||||
"lastAccessTimeFormatted": "13 days ago",
|
||||
"scope": ["openid", "profile"]
|
||||
},
|
||||
{
|
||||
"id": "23d10a14f474ca41",
|
||||
"name": "Example Two",
|
||||
"lastAccessTime": 1476677854037,
|
||||
"lastAccessTimeFormatted": "2 years ago",
|
||||
"scope": ["profile:email", "profile:uid"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### DELETE /v1/client-tokens/:id
|
||||
|
||||
This endpoint deletes all tokens granted to a given client.
|
||||
It must be authenticated with an OAuth token bearing scope "clients:write".
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- `id`: The `client_id` of the client whose tokens should be deleted.
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -X DELETE
|
||||
https://oauth.accounts.firefox.com/v1/client-tokens/5901bd09376fadaa
|
||||
-H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid 200 response will return an empty JSON object.
|
||||
|
||||
|
||||
|
||||
[client]: #get-v1clientid
|
||||
[register]: #post-v1clientregister
|
||||
[clients]: #get-v1clients
|
||||
[client-update]: #post-v1clientid
|
||||
[client-delete]: #delete-v1clientid
|
||||
[redirect]: #get-v1authorization
|
||||
[authorization]: #post-v1authorization
|
||||
[token]: #post-v1token
|
||||
[delete]: #post-v1destroy
|
||||
[verify]: #post-v1verify
|
||||
[developer-activate]: #post-v1developeractivate
|
||||
[jwks]: #get-v1jwks
|
||||
[key-data]: #post-v1post-keydata
|
||||
[client-tokens]: #get-v1client-tokens
|
||||
[client-tokens-delete]: #delete-v1client-tokensid
|
||||
|
||||
[Service Clients]: ./service-clients.md
|
|
@ -1,68 +0,0 @@
|
|||
# Configuring OAuth clients
|
||||
|
||||
## How to register a client manually
|
||||
|
||||
Usually, when you connect applications to their OAuth resource server, they generate a client `id` and `secret` for you. In our case, we are the resource server.
|
||||
|
||||
The `id` and `secret` keys, in this context, can be seen as a username and password. They do not need to be generated in relation between one and another. In other words, they are not public and private keys.
|
||||
|
||||
With this procedure you will generate both client id and secret tokens to provide to your other applications and also partners who wants to leverage your identity provider service. Once you have the client id and secret, paste them in both the fxa-oauth-server AND your client service you want to bind using OAuth.
|
||||
|
||||
|
||||
## Difference between same site and external consumers
|
||||
|
||||
While other applications within your infrastructure would ideally be pre-approved at the user point of view, external consumers shouldn't be. This is why when we develop a service leveraging another site, the user gets a confirmation window.
|
||||
|
||||
If you want to pre-approve your own web applications and prevent users in your accounts userbase to have a confirmation window, set the `trusted` flag to `true`.
|
||||
|
||||
|
||||
## Installing a new consumer
|
||||
|
||||
### Creating the client id and secret keys
|
||||
|
||||
Use the [fxa-oauth-client][] CLI tool for registering new clients with your server.
|
||||
|
||||
FxA OAuth development environments support `127.0.0.1` and `localhost` as valid `redirectUri` values to ease development.
|
||||
|
||||
[fxa-oauth-client]: https://github.com/mozilla/fxa-oauth-client
|
||||
|
||||
### OAuth resource server (a.k.a. `fxa-oauth-server`)
|
||||
|
||||
Let's assume that the client in this example is the [123done](https://github.com/mozilla/123done) web application that you deployed at `https://clientapp.example.com` and that the OAuth route is available at `api/oauth` (this is specific to each client application, beware (!))
|
||||
|
||||
Add a new object literal within the `clients` array, that would look like:
|
||||
|
||||
```json
|
||||
{
|
||||
"clients": [
|
||||
{
|
||||
"id": "<8-byte client id in hex>",
|
||||
"hashedSecret": "<32-byte sha256 of the client secret in hex>",
|
||||
"name": "123done",
|
||||
"imageUri": "https://clientapp.example.com/static/img/logo100.png",
|
||||
"redirectUri": "https://clientapp.example.com/api/oauth",
|
||||
"trusted": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**NOTE:** the `trusted`, this would be for an internal application that you manage.
|
||||
|
||||
|
||||
### OAuth clients
|
||||
|
||||
This can be very different depending on your installed and supported version, you should have a look at the profile server and client application what are the required callbacks.
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "<8-byte client id in hex>",
|
||||
"client_secret": "<32-byte client secret in hex>",
|
||||
"name": "123done",
|
||||
"redirect_uri": "https://clientapp.example.com/api/oauth",
|
||||
"signin_uri": "https://accounts.firefox.com/oauth/signin",
|
||||
"oauth_uri": "https://oauth.accounts.firefox.com/v1",
|
||||
"profile_uri": "https://profile.firefox.com/v1",
|
||||
"scopes": "profile"
|
||||
}
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
# Firefox Accounts OAuth - PKCE Support
|
||||
|
||||
> Proof Key for Code Exchange by OAuth Public Clients
|
||||
|
||||
Firefox Accounts OAuth flow supports the [PKCE RFC7636](https://tools.ietf.org/html/rfc7636).
|
||||
This feature helps us authenticate clients such as WebExtensions and Native apps.
|
||||
Clients that do not have a server component or a secure way to store a `client_secret`.
|
||||
|
||||
To better understand this protocol please read the [Proof Key for Code Exchange (RFC 7636) by Authlete Inc.](https://www.authlete.com/documents/article/pkce/index).
|
||||
|
||||
Please see the [API](API.md) documentation that explains the support parameters - `code_challenge_method`, `code_challenge` and `code_verifier`.
|
||||
|
||||
At this time Firefox Accounts requires you to use the `S256` flow, we do not support the `plain` code challenge method.
|
|
@ -1,199 +0,0 @@
|
|||
# OAuth Scopes
|
||||
|
||||
Each authorization grant in OAuth has an associated "scope",
|
||||
a list containing one or more "scope values"
|
||||
that indicate what capabilities the granted token will have.
|
||||
Each individual scope value indicates a particular capability,
|
||||
such as the ability to read or write profile data,
|
||||
or to access the user's data in a particular service.
|
||||
|
||||
As defined in [RFC6749 Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3),
|
||||
the scope of a token is expressed as
|
||||
a list of space-delimited, case-sensitive strings,
|
||||
and it is left up to the service
|
||||
to define the format and semantics
|
||||
of the individual scope values
|
||||
that make up this string.
|
||||
|
||||
This document defines the scope values
|
||||
accepted in the Firefox Accounts ecosystem,
|
||||
and the rules for parsing and validating them.
|
||||
|
||||
## Short-name scope values
|
||||
|
||||
FxA supports a small set of "short-name" scope values
|
||||
that are identified by a short English word.
|
||||
These correspond either to
|
||||
scope values defined by external specifications
|
||||
(e.g. OpenID Connect),
|
||||
or to legacy scope values
|
||||
introduced during early development.
|
||||
|
||||
**No new short-name scope values should be added.**
|
||||
Instead we prefer to use URLs for new scope values,
|
||||
both to ensure uniqueness
|
||||
and to simplify parsing rules.
|
||||
|
||||
Short-name scope values imply read-only access by default,
|
||||
with write access indicated by the suffix ":write".
|
||||
The may also have "sub-scopes"
|
||||
to indicate finer-grained access control.
|
||||
Each name component may contain only ascii alphanumeric characters
|
||||
and the underscore.
|
||||
|
||||
For example:
|
||||
|
||||
* `profile` indicates read-only access
|
||||
to the user's profile data.
|
||||
* `profile:write` indicates read/write access
|
||||
to the user's profile data.
|
||||
* `profile:display_name` indicates read-only access
|
||||
to the user's display name, but not any other
|
||||
profile data.
|
||||
* `profile:email:write` indicates read/write access
|
||||
to the user's email address.
|
||||
|
||||
The following short-name scope values are recognized
|
||||
in the FxA ecosystem.
|
||||
|
||||
### Profile data
|
||||
|
||||
* `profile`: access the user's profile data.
|
||||
* `profile:uid`: access the user's opaque user id.
|
||||
* `profile:email`: access the user's email address.
|
||||
* `profile:locale`: access the user's locale.
|
||||
* `profile:avatar`: access the user's avatar picture.
|
||||
* `profile:display_name`: access the user's human-readable display name.
|
||||
* `profile:amr`: access information about the user's authentication methods and 2FA status.
|
||||
|
||||
### OpenID Connect
|
||||
|
||||
* `openid`: used to request an OpenID Connect `id_token`.
|
||||
* `email`: a synonym for `profile:email`, defined by the OIDC spec.
|
||||
|
||||
### OAuth Client Management
|
||||
|
||||
* `clients`: access the list of OAuth clients connected to a user's account.
|
||||
* `oauth`: register a new OAuth client record.
|
||||
|
||||
### Basket
|
||||
|
||||
* `basket`: access the user's subscription data in
|
||||
[basket](http://basket.readthedocs.io/)
|
||||
|
||||
|
||||
## URL Scopes
|
||||
|
||||
For new capabilities, scope values are represented as URLs.
|
||||
This helps to ensure uniqueness
|
||||
and reduces ambiguity in parsing.
|
||||
URL-format scope value imply read/write access by default,
|
||||
are compared as heirarchical resource references,
|
||||
and use the hash fragment for permission qualifiers.
|
||||
For example:
|
||||
|
||||
* `https://identity.mozilla.com/apps/oldsync` indicates full
|
||||
access to the user's data in Firefox Sync.
|
||||
* `https://identity.mozilla.com/apps/oldsync/bookmarks` indicates
|
||||
full access to the user's bookmark data in Firefox Sync,
|
||||
but not to other data types.
|
||||
* `https://identity.mozilla.com/apps/oldsync#read` indicates
|
||||
read-only access to the user's data in Firefox Sync.
|
||||
* `https://identity.mozilla.com/apps/oldsync/history#write` indicates
|
||||
write-only access to the user's history data in Firefox Sync.
|
||||
|
||||
To be a valid scope value, the URL must:
|
||||
|
||||
* Be an absolute `https://` URL.
|
||||
* Have no username, password, or query component.
|
||||
* If present, have a fragment component consisting only of alphanumeric ascii characters and underscore.
|
||||
* Remain unchanged when parsed and serialized following the rules in the
|
||||
[WhatWG URL Spec](https://url.spec.whatwg.org).
|
||||
|
||||
The following URL scope values are currently recognized by FxA:
|
||||
|
||||
* `https://identity.mozilla.com/apps/oldsync`: access to data in Firefox Sync.
|
||||
* `https://identity.mozilla.com/apps/notes`: access to data in Firefox Notes.
|
||||
|
||||
|
||||
## Scope Matching and Implication
|
||||
|
||||
We say that a scope value A *implies* another scope value B
|
||||
if they are exactly equal,
|
||||
or if A represents a more general capability than B.
|
||||
Similarly, a scope A implies scope value B
|
||||
if there is some scope value in A that implies B.
|
||||
This is the basic operation used to check
|
||||
permissions when processing an OAuth token.
|
||||
|
||||
Consumers of OAuth tokens should avoid
|
||||
directly parsing and comparing scopes where possible,
|
||||
and instead use the existing implementation
|
||||
in the `fxa-shared` node module.
|
||||
|
||||
For consumers that must implement their own scope checking,
|
||||
the rules for implication can be summarized as:
|
||||
|
||||
* For URL scope values, A implies B if A is a parent resource of B.
|
||||
* For short-name scope values, split on the ":" character,
|
||||
and A implies B if either:
|
||||
* B[-1] is not "write" and A is a prefix of B, or.
|
||||
* A[-1] is "write", and:
|
||||
* A[:-1] is a prefix of B, or
|
||||
* B[-1] is "write" and A[:-1] is a prefix of B[:-1]
|
||||
|
||||
More precisely, the algoritm for checking implication is:
|
||||
|
||||
* If A is a `https://` URL, then:
|
||||
* If B is not a `https://` URL, then fail.
|
||||
* If the origin of B is different than that of A, then fail.
|
||||
* If the path component list of A is not a prefix of the path
|
||||
component list of B, then fail.
|
||||
* If A has a fragment, then:
|
||||
* If B does not have a fragment, then fail.
|
||||
* If B has a fragment that differs from A, then fail.
|
||||
* Otherwise, succeed.
|
||||
* Otherwise:
|
||||
* If B is a `https://` URL, then fail.
|
||||
* Split A and B into components based on `:` delimiter.
|
||||
* If the last component of B is `write`, then:
|
||||
* If the last component of A is not `write`, then fail.
|
||||
* If the last component of A is `write`, remove it.
|
||||
* If A is not a prefix of B, then fail.
|
||||
* Otherwise, succeed.
|
||||
|
||||
Below are some testcases against which
|
||||
scope-checking code can be validated.
|
||||
|
||||
Valid implications:
|
||||
* `profile:write` implies `profile`.
|
||||
* `profile` implies `profile:email`.
|
||||
* `profile:write` implies `profile:email`.
|
||||
* `profile:write` implies `profile:email:write`.
|
||||
* `profile:email:write` implies `profile:email`.
|
||||
* `profile profile:email:write` implies `profile:email`.
|
||||
* `profile profile:email:write` implies `profile:display_name`.
|
||||
* `profile https://identity.mozilla.com/apps/oldsync` implies `profile`.
|
||||
* `profile https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync`.
|
||||
* `https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync#read`.
|
||||
* `https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync/bookmarks`.
|
||||
* `https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync/bookmarks#read`.
|
||||
* `https://identity.mozilla.com/apps/oldsync#read` implies `https://identity.mozilla.com/apps/oldsync/bookmarks#read`.
|
||||
* `https://identity.mozilla.com/apps/oldsync#read profile` implies `https://identity.mozilla.com/apps/oldsync/bookmarks#read`.
|
||||
|
||||
Invalid implications:
|
||||
* `profile:email:write` does *not* imply `profile`.
|
||||
* `profile:email:write` does *not* imply `profile:write`.
|
||||
* `profile:email` does *not* imply `profile:display_name`.
|
||||
* `profilebogey` does *not* imply `profile`.
|
||||
* `profile:write` does *not* imply `https://identity.mozilla.com/apps/oldsync`.
|
||||
* `profile profile:email:write` does *not* imply `profile:write`.
|
||||
* `https` does *not* imply `https://identity.mozilla.com/apps/oldsync`.
|
||||
* `https://identity.mozilla.com/apps/oldsync` does *not* imply `profile`.
|
||||
* `https://identity.mozilla.com/apps/oldsync#read` does *not* imply `https://identity.mozilla.com/apps/oldsync/bookmarks`.
|
||||
* `https://identity.mozilla.com/apps/oldsync#write` does *not* imply `https://identity.mozilla.com/apps/oldsync/bookmarks#read`.
|
||||
* `https://identity.mozilla.com/apps/oldsync/bookmarks` does *not* imply `https://identity.mozilla.com/apps/oldsync`.
|
||||
* `https://identity.mozilla.com/apps/oldsync/bookmarks` does *not* imply `https://identity.mozilla.com/apps/oldsync/passwords`.
|
||||
* `https://identity.mozilla.com/apps/oldsyncer` does *not* imply `https://identity.mozilla.com/apps/oldsync`.
|
||||
* `https://identity.mozilla.com/apps/oldsync` does *not* imply `https://identity.mozilla.com/apps/oldsyncer`.
|
||||
* `https://identity.mozilla.org/apps/oldsync` does *not* imply `https://identity.mozilla.com/apps/oldsync`.
|
|
@ -1,92 +0,0 @@
|
|||
# Service Clients
|
||||
|
||||
Do you wish to allow users to authenticate to your app, as well as fetch
|
||||
some information about them? Then you don't want to be a Service Client.
|
||||
Be a regular client.
|
||||
|
||||
Service Clients exist as privileged apps that need to be able to request
|
||||
information about a user, without ever having received permission to do
|
||||
so. Sounds nefarious. However, as said, these are **privileged** apps,
|
||||
meaning they are also run by **us** (Mozilla). So these apps are not
|
||||
gaining access to something that we don't already know. Plus, before
|
||||
ever being promoted to a Service Client, real humans are involved to
|
||||
make sure it's the correct use case.
|
||||
|
||||
## All Powerful
|
||||
|
||||
A Service Client exists by being in the config array `serviceClients`.
|
||||
Each entry (if any) is an object, with the following values:
|
||||
|
||||
- `id` - `String`: a unique hex value in identical format to regular
|
||||
client ids.
|
||||
- `name` - `String`: a plaintext name for the client that's friendly to
|
||||
human readers.
|
||||
- `scope` - `String`: space-separated scopes that this client will be
|
||||
using when requesting access tokens.
|
||||
- `jku` - `String`: a unique URL that will host a JWK Set. Unique as in
|
||||
unique in the current list of Service Clients.
|
||||
|
||||
## JKUs and JWK Sets
|
||||
|
||||
Imagine a service client with the `jku` of `https://example.dom.ain/keys`.
|
||||
The document hosted at that URL should contain something like:
|
||||
|
||||
```json
|
||||
{
|
||||
"keys":[{
|
||||
"kid":"key-id-can-whatever-1",
|
||||
"use": "sig",
|
||||
"kty":"RSA",
|
||||
"n":"W_lCUvksZMVxW2JLNtoyPPshvSHng28H5FggSBGBjmzv3eHkMgRdc8hpOkgcPwXYxHdVM6udtVdXZtbGN8nUyQX8gxD3AJg-GSrH3UOsoArPLCmcxwIEpk4B0wqwP68oK8dQHt0iK3N-XeCnMpv75ULlVn3LEOZT8CsuNraVOthYeClUb8r1PjRwqRB06QGNqnnhcPMmh-6cRzQ9HmTMz6CDcugiH5n2sjrvpeBugEsnXt3KpzVdSc4usXrIEmLRuFjwFbkzoo7FiAtSoXxBqc074qz8ejm-V0-2Wv3p6ePeLODeYkPQho4Lb1TBdoidr9RHY29Out4mhzb4nUrHHQ",
|
||||
"e":"AQAB"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
This JKU is important when making requests for access tokens.
|
||||
|
||||
## JWTs and Authorization
|
||||
|
||||
To request an access token, a service client must generate a signed [JWT][],
|
||||
and then send it to our [/v1/token][] endpoint.
|
||||
|
||||
Here's an example of creating a JWT in JavaScript:
|
||||
|
||||
```js
|
||||
var now = Math.floor(Date.now() / 1000); // in seconds
|
||||
var header = {
|
||||
alg: 'RS256',
|
||||
typ: 'JWT',
|
||||
jku: 'https://basket.mozilla.org/.well-known/jku',
|
||||
kid: 'k1'
|
||||
};
|
||||
var claims = {
|
||||
scope: 'profile:email',
|
||||
aud: 'https://oauth.accounts.firefox.com/v1/token',
|
||||
iat: now,
|
||||
exp: now + (60 * 5),
|
||||
sub: '9b052aebbc48c8376257c777e2a7f009'
|
||||
};
|
||||
|
||||
var token = base64(JSON.stringify(header)) + '.' + base64(JSON.stringify(claims));
|
||||
var sig = rsa256(Buffer.from(token, 'base64'), privateKey);
|
||||
var jwt = token + '.' + base64(sig);
|
||||
```
|
||||
|
||||
Once you have a signed JWT, you would make the following request:
|
||||
|
||||
```sh
|
||||
curl -v \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://oauth.accounts.firefox.com/v1/token" \
|
||||
-d '{
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
"assertion": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHBzOi8vYmFza2V0LmFjY291bnRzLmZpcmVmb3guY29tLy53ZWxsLWtub3duL2prdSIsImtpZCI6ImsxIn0.eyJzdWIiOiI1OTAxYmQwOTM3NmZhZGFhNTkwMWJkMDkzNzZmYWRhYUBhY2NvdW50cy5maXJlZm94LmNvbSIsInNjb3BlIjoicHJvZmlsZTplbWFpbCIsImF1ZCI6Imh0dHBzOi8vb2F1dGguYWNjb3VudHMuZmlyZWZveC5jb20vdjEvdG9rZW4iLCJpYXQiOjE0NDM2NjI0ODEsImV4cCI6MTQ0MzY2Mjc4MX0.Kmwfq7yZrKpwrcZ78NTLPs8v4ijMhoKVNZ45VJY-skyK_XD_U5DJeKq8IE6PspU6B6p0DPkW1EEKeKOAbpyzFIBi9uG7l329x32JkzXGwybxannbGrdd5DFZbIaBSZDf-64MXbxGBGQ8xy18dfXmgbmNsvYPRZqqS2gmoM1EvWg"
|
||||
}'
|
||||
```
|
||||
|
||||
The response is described in the [API docs][/v1/token].
|
||||
|
||||
[/v1/token]: ./api.md#post-v1token
|
||||
[JWT]: http://jwt.io/
|
|
@ -1,25 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// takes care of bumping the version number in package.json
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
|
||||
grunt.config('bump', {
|
||||
options: {
|
||||
files: ['package.json', 'npm-shrinkwrap.json'],
|
||||
bumpVersion: true,
|
||||
commit: true,
|
||||
commitMessage: 'Release v%VERSION%',
|
||||
commitFiles: ['package.json', 'npm-shrinkwrap.json', 'CHANGELOG.md'],
|
||||
createTag: true,
|
||||
tagName: 'v%VERSION%',
|
||||
tagMessage: 'Version %VERSION%',
|
||||
push: false,
|
||||
pushTo: 'origin',
|
||||
gitDescribeOptions: '--tags --always --abrev=1 --dirty=-d'
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var fxaChangelog = require('fxa-conventional-changelog')();
|
||||
|
||||
module.exports = function (grunt) {
|
||||
grunt.config('conventionalChangelog', {
|
||||
options: {
|
||||
changelogOpts: {},
|
||||
parserOpts: fxaChangelog.parserOpts,
|
||||
writerOpts: fxaChangelog.writerOpts
|
||||
},
|
||||
release: {
|
||||
src: 'CHANGELOG.md'
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
|
||||
grunt.config('copyright', {
|
||||
app: {
|
||||
options: {
|
||||
pattern: /This Source Code Form is subject to the terms of the Mozilla/
|
||||
},
|
||||
src: [
|
||||
'<%= mainJsFiles %>'
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
|
||||
grunt.config('eslint', {
|
||||
options: {
|
||||
eslintrc: '.eslintrc'
|
||||
},
|
||||
app: [
|
||||
'<%= mainJsFiles %>'
|
||||
]
|
||||
});
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// meta grunt task to run other linters.
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
|
||||
grunt.registerTask('lint', [
|
||||
'eslint'
|
||||
]);
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//
|
||||
// A task to stamp a new version.
|
||||
//
|
||||
//
|
||||
// * version is updated in package.json
|
||||
// * changelog is generated.
|
||||
// * git tag with version name is created.
|
||||
// * git commit with updated package.json created.
|
||||
//
|
||||
// NOTE: This task will not push this commit for you.
|
||||
//
|
||||
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
|
||||
grunt.registerTask('version', [
|
||||
'bump-only:minor',
|
||||
'conventionalChangelog:release',
|
||||
'bump-commit'
|
||||
]);
|
||||
|
||||
grunt.registerTask('version:patch', [
|
||||
'bump-only:patch',
|
||||
'conventionalChangelog:release',
|
||||
'bump-commit'
|
||||
]);
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
|
||||
grunt.config('nodemon', {
|
||||
dev: {
|
||||
script: 'bin/server.js',
|
||||
options: {
|
||||
args: ['--node-env=dev']
|
||||
}
|
||||
}
|
||||
});
|
||||
grunt.registerTask('server', ['nodemon:dev']);
|
||||
};
|
|
@ -1,60 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
const AppError = require('./error');
|
||||
const logger = require('./logging')('server.auth');
|
||||
const token = require('./token');
|
||||
const validators = require('./validators');
|
||||
|
||||
const WHITELIST = require('./config').get('admin.whitelist').map(function(re) {
|
||||
logger.verbose('compiling.whitelist', re);
|
||||
return new RegExp(re);
|
||||
});
|
||||
|
||||
exports.AUTH_STRATEGY = 'dogfood';
|
||||
exports.AUTH_SCHEME = 'bearer';
|
||||
|
||||
exports.SCOPE_CLIENT_MANAGEMENT = ScopeSet.fromArray(['oauth']);
|
||||
|
||||
exports.strategy = function() {
|
||||
return {
|
||||
authenticate: async function dogfoodStrategy(req, h) {
|
||||
var auth = req.headers.authorization;
|
||||
logger.debug('check.auth', { header: auth });
|
||||
if (! auth || auth.indexOf('Bearer ') !== 0) {
|
||||
throw AppError.unauthorized('Bearer token not provided');
|
||||
}
|
||||
var tok = auth.split(' ')[1];
|
||||
|
||||
if (! validators.HEX_STRING.test(tok)) {
|
||||
throw AppError.unauthorized('Illegal Bearer token');
|
||||
}
|
||||
|
||||
return token.verify(tok).then(function tokenFound(details) {
|
||||
if (details.scope.contains(exports.SCOPE_CLIENT_MANAGEMENT)) {
|
||||
logger.debug('check.whitelist');
|
||||
var blocked = ! WHITELIST.some(function(re) {
|
||||
return re.test(details.email);
|
||||
});
|
||||
if (blocked) {
|
||||
logger.warn('whitelist.blocked', {
|
||||
email: details.email,
|
||||
token: tok
|
||||
});
|
||||
throw AppError.forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('success', details);
|
||||
details.scope = details.scope.getScopeValues();
|
||||
return h.authenticated({credentials: details});
|
||||
}, function noToken(err) {
|
||||
logger.debug('error', err);
|
||||
throw AppError.unauthorized('Bearer token invalid');
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,47 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
const AppError = require('./error');
|
||||
const logger = require('./logging')('server.auth_bearer');
|
||||
const token = require('./token');
|
||||
const validators = require('./validators');
|
||||
|
||||
const authName = 'authBearer';
|
||||
const authOAuthScope = ScopeSet.fromArray(['clients:write']);
|
||||
|
||||
exports.AUTH_STRATEGY = authName;
|
||||
exports.AUTH_SCHEME = authName;
|
||||
|
||||
exports.SCOPE_CLIENT_WRITE = authOAuthScope;
|
||||
|
||||
exports.strategy = function() {
|
||||
return {
|
||||
authenticate: async function authBearerStrategy(req, h) {
|
||||
var auth = req.headers.authorization;
|
||||
|
||||
logger.debug(authName + '.check', { header: auth });
|
||||
if (! auth || auth.indexOf('Bearer ') !== 0) {
|
||||
throw AppError.unauthorized('Bearer token not provided');
|
||||
}
|
||||
var tok = auth.split(' ')[1];
|
||||
|
||||
if (! validators.HEX_STRING.test(tok)) {
|
||||
throw AppError.unauthorized('Illegal Bearer token');
|
||||
}
|
||||
|
||||
return token.verify(tok).then(function tokenFound(details) {
|
||||
logger.info(authName + '.success', details);
|
||||
details.scope = details.scope.getScopeValues();
|
||||
return h.authenticated({
|
||||
credentials: details
|
||||
});
|
||||
}, function noToken(err) {
|
||||
logger.debug(authName + '.error', err);
|
||||
throw AppError.unauthorized('Bearer token invalid');
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,105 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
|
||||
const Joi = require('joi');
|
||||
|
||||
const AppError = require('./error');
|
||||
const config = require('./config');
|
||||
const logger = require('./logging')('assertion');
|
||||
const P = require('./promise');
|
||||
|
||||
const HEX_STRING = /^[0-9a-f]+$/;
|
||||
const CLAIMS_SCHEMA = Joi.object({
|
||||
'uid': Joi.string().length(32).regex(HEX_STRING).required(),
|
||||
'fxa-generation': Joi.number().integer().min(0).required(),
|
||||
'fxa-verifiedEmail': Joi.string().max(255).required(),
|
||||
'fxa-lastAuthAt': Joi.number().integer().min(0).required(),
|
||||
'fxa-tokenVerified': Joi.boolean().optional(),
|
||||
'fxa-amr': Joi.array().items(Joi.string().alphanum()).optional(),
|
||||
'fxa-aal': Joi.number().integer().min(0).max(3).optional(),
|
||||
'fxa-profileChangedAt': Joi.number().integer().min(0).optional()
|
||||
}).options({ stripUnknown: true });
|
||||
|
||||
const AUDIENCE = config.get('publicUrl');
|
||||
|
||||
const request = require('request').defaults({
|
||||
url: config.get('browserid.verificationUrl'),
|
||||
pool: {
|
||||
maxSockets: config.get('browserid.maxSockets')
|
||||
}
|
||||
});
|
||||
|
||||
function unb64(text) {
|
||||
return Buffer.from(text, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
function Assertion(assertion) {
|
||||
this.assertion = assertion;
|
||||
}
|
||||
|
||||
Assertion.prototype.toJSON = function() {
|
||||
const parts = this.assertion.split('.');
|
||||
const ass = JSON.parse(unb64(parts[1]));
|
||||
ass.pubkey = ass.publicKey = ass['public-key'] = undefined;
|
||||
try {
|
||||
return {
|
||||
header: JSON.parse(unb64(parts[0])),
|
||||
assertion: ass,
|
||||
cert: JSON.parse(unb64(parts[3]))
|
||||
};
|
||||
} catch (ex) {
|
||||
return ex;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = function verifyAssertion(assertion) {
|
||||
logger.verbose('assertion', new Assertion(assertion));
|
||||
const d = P.defer();
|
||||
const opts = {
|
||||
json: {
|
||||
assertion: assertion,
|
||||
audience: AUDIENCE
|
||||
}
|
||||
};
|
||||
request.post(opts, (err, res, body) => {
|
||||
if (err) {
|
||||
logger.error('verify.error', err);
|
||||
return d.reject(err);
|
||||
}
|
||||
|
||||
function error(msg, val) {
|
||||
logger.info('invalidAssertion', {
|
||||
msg: msg,
|
||||
val: val,
|
||||
assertion: assertion
|
||||
});
|
||||
d.reject(AppError.invalidAssertion());
|
||||
}
|
||||
|
||||
if (! body || body.status !== 'okay') {
|
||||
return error('non-okay response', body);
|
||||
}
|
||||
const email = body.email;
|
||||
const parts = email.split('@');
|
||||
const allowedIssuer = config.get('browserid.issuer');
|
||||
if (parts.length !== 2 || parts[1] !== allowedIssuer) {
|
||||
return error('invalid email', email);
|
||||
}
|
||||
if (body.issuer !== allowedIssuer) {
|
||||
return error('invalid issuer', body.issuer);
|
||||
}
|
||||
const uid = parts[0];
|
||||
|
||||
const claims = body.idpClaims || {};
|
||||
claims.uid = uid;
|
||||
CLAIMS_SCHEMA.validate(claims, (err, claims) => {
|
||||
if (err) {
|
||||
return error(err, claims);
|
||||
}
|
||||
return d.resolve(claims);
|
||||
});
|
||||
});
|
||||
return d.promise;
|
||||
};
|
|
@ -1,406 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const convict = require('convict');
|
||||
var DEFAULT_SUPPORTED_LANGUAGES = require('fxa-shared').l10n.supportedLanguages;
|
||||
|
||||
const conf = convict({
|
||||
admin: {
|
||||
whitelist: {
|
||||
doc: 'An array of regexes. Passing any one will get through.',
|
||||
default: ['@mozilla\\.com$']
|
||||
}
|
||||
},
|
||||
api: {
|
||||
version: {
|
||||
doc: 'Number part of versioned endpoints - ex: /v1/token',
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
allowHttpRedirects: {
|
||||
arg: 'allowHttpRedirects',
|
||||
doc: 'If true, then it allows http OAuth redirect uris',
|
||||
env: 'ALLOW_HTTP_REDIRECTS',
|
||||
format: Boolean,
|
||||
default: false
|
||||
},
|
||||
browserid: {
|
||||
issuer: {
|
||||
doc: 'We only accept assertions from this issuer',
|
||||
env: 'ISSUER',
|
||||
default: 'api.accounts.firefox.com'
|
||||
},
|
||||
maxSockets: {
|
||||
doc: 'The maximum number of connections that the pool can use at once.',
|
||||
env: 'BROWSERID_MAX_SOCKETS',
|
||||
default: 10
|
||||
},
|
||||
verificationUrl: {
|
||||
doc: 'URL to the remote verifier we will use for fxa-assertions',
|
||||
format: 'url',
|
||||
env: 'VERIFICATION_URL',
|
||||
default: 'https://verifier.accounts.firefox.com/v2'
|
||||
}
|
||||
},
|
||||
clients: {
|
||||
doc: 'Some pre-defined clients that will be inserted into the DB',
|
||||
env: 'OAUTH_CLIENTS',
|
||||
default: []
|
||||
},
|
||||
scopes: {
|
||||
doc: 'Some pre-defined list of scopes that will be inserted into the DB',
|
||||
env: 'OAUTH_SCOPES',
|
||||
default: []
|
||||
},
|
||||
clientAddressDepth: {
|
||||
doc: 'location of the client ip address in the remote address chain',
|
||||
format: Number,
|
||||
env: 'CLIENT_ADDRESS_DEPTH',
|
||||
default: 3
|
||||
},
|
||||
contentUrl: {
|
||||
doc: 'URL to UI page in fxa-content-server that starts OAuth flow',
|
||||
format: 'url',
|
||||
env: 'CONTENT_URL',
|
||||
default: 'https://accounts.firefox.com/oauth/'
|
||||
},
|
||||
db: {
|
||||
driver: {
|
||||
env: 'DB',
|
||||
format: ['mysql', 'memory'],
|
||||
default: 'memory'
|
||||
},
|
||||
autoUpdateClients: {
|
||||
doc: 'If true, update clients from config file settings',
|
||||
env: 'DB_AUTO_UPDATE_CLIENTS',
|
||||
format: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
encrypt: {
|
||||
hashAlg: {
|
||||
doc: 'Hash algorithm for storing secrets/codes/tokens.',
|
||||
default: 'sha256'
|
||||
}
|
||||
},
|
||||
env: {
|
||||
arg: 'node-env',
|
||||
doc: 'The current node.js environment',
|
||||
env: 'NODE_ENV',
|
||||
format: ['dev', 'test', 'stage', 'prod'],
|
||||
default: 'prod'
|
||||
},
|
||||
events: {
|
||||
region: {
|
||||
doc: 'AWS Region of fxa account events',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'FXA_EVENTS_REGION'
|
||||
},
|
||||
queueUrl: {
|
||||
doc: 'SQS queue url for fxa account events',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'FXA_EVENTS_QUEUE_URL'
|
||||
}
|
||||
},
|
||||
expiration: {
|
||||
accessToken: {
|
||||
doc: 'Access Tokens maximum expiration (can live shorter)',
|
||||
format: 'duration',
|
||||
default: '2 weeks',
|
||||
env: 'FXA_EXPIRATION_ACCESS_TOKEN'
|
||||
},
|
||||
accessTokenExpiryEpoch: {
|
||||
doc: 'Timestamp after which access token expiry is actively enforced',
|
||||
format: 'timestamp',
|
||||
default: '2017-01-01',
|
||||
env: 'FXA_EXPIRATION_ACCESS_TOKEN_EXPIRY_EPOCH'
|
||||
},
|
||||
code: {
|
||||
doc: 'Clients must trade codes for tokens before they expire',
|
||||
format: 'duration',
|
||||
default: '15 minutes',
|
||||
env: 'FXA_EXPIRATION_CODE'
|
||||
},
|
||||
keyDataAuth: {
|
||||
doc: 'Key data can only be fetched if lastAuthAt is within this duration',
|
||||
format: 'duration',
|
||||
default: '1 hour',
|
||||
env: 'FXA_EXPIRATION_KEY_DATA_AUTH'
|
||||
}
|
||||
},
|
||||
refreshToken: {
|
||||
updateAfter: {
|
||||
doc: 'lastUsedAt only gets updated after this delay',
|
||||
format: 'duration',
|
||||
default: '24 hours',
|
||||
env: 'FXA_REFRESH_TOKEN_UPDATE_AFTER'
|
||||
}
|
||||
},
|
||||
git: {
|
||||
commit: {
|
||||
doc: 'Commit SHA when in stage/production',
|
||||
format: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
hpkpConfig: {
|
||||
enabled: {
|
||||
default: false,
|
||||
doc: 'Feature flag for appending HPKP headers',
|
||||
format: Boolean,
|
||||
env: 'HPKP_ENABLE'
|
||||
},
|
||||
reportOnly: {
|
||||
default: true,
|
||||
doc: 'Enable report only mode',
|
||||
format: Boolean,
|
||||
env: 'HPKP_REPORT_ONLY'
|
||||
},
|
||||
reportUri: {
|
||||
default: '',
|
||||
doc: 'Enable report only mode',
|
||||
format: String,
|
||||
env: 'HPKP_REPORT_URI'
|
||||
},
|
||||
includeSubDomains: {
|
||||
default: true,
|
||||
doc: 'Include Sub-Domains',
|
||||
format: Boolean,
|
||||
env: 'HPKP_INCLUDE_SUBDOMAINS'
|
||||
},
|
||||
maxAge: {
|
||||
default: 1,
|
||||
doc: 'Max age for HPKP headers (seconds)',
|
||||
format: Number,
|
||||
env: 'HPKP_MAX_AGE'
|
||||
},
|
||||
sha256s: {
|
||||
default: [],
|
||||
doc: 'Supported pin-sha256s',
|
||||
format: Array,
|
||||
env: 'HPKP_PIN_SHA256'
|
||||
}
|
||||
},
|
||||
localRedirects: {
|
||||
doc: 'When true, `localhost` and `127.0.0.1` always are legal redirects.',
|
||||
default: false,
|
||||
env: 'FXA_OAUTH_LOCAL_REDIRECTS'
|
||||
},
|
||||
logging: {
|
||||
app: {
|
||||
default: 'fxa-oauth-server',
|
||||
env: 'LOG_APP'
|
||||
},
|
||||
level: {
|
||||
default: 'info',
|
||||
env: 'LOG_LEVEL'
|
||||
},
|
||||
fmt: {
|
||||
format: ['heka', 'pretty'],
|
||||
default: 'heka',
|
||||
env: 'LOG_FORMAT'
|
||||
}
|
||||
},
|
||||
mysql: {
|
||||
createSchema: {
|
||||
default: true,
|
||||
env: 'CREATE_MYSQL_SCHEMA'
|
||||
},
|
||||
user: {
|
||||
default: 'root',
|
||||
env: 'MYSQL_USERNAME'
|
||||
},
|
||||
password: {
|
||||
default: '',
|
||||
env: 'MYSQL_PASSWORD'
|
||||
},
|
||||
database: {
|
||||
default: 'fxa_oauth',
|
||||
env: 'MYSQL_DATABASE'
|
||||
},
|
||||
host: {
|
||||
default: '127.0.0.1',
|
||||
env: 'MYSQL_HOST'
|
||||
},
|
||||
port: {
|
||||
default: '3306',
|
||||
env: 'MYSQL_PORT'
|
||||
},
|
||||
connectionLimit: {
|
||||
doc: 'The maximum number of connections that the pool can use at once.',
|
||||
default: 10,
|
||||
env: 'MYSQL_CONNECTION_LIMIT'
|
||||
}
|
||||
},
|
||||
openid: {
|
||||
keyFile: {
|
||||
doc: 'Path to Private key JWK to sign id_tokens',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'FXA_OPENID_KEYFILE'
|
||||
},
|
||||
oldKeyFile: {
|
||||
doc: 'Path to previous key that was used to sign id_tokens',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'FXA_OPENID_OLDKEYFILE'
|
||||
},
|
||||
key: {
|
||||
doc: 'Private JWK to sign id_tokens',
|
||||
default: {},
|
||||
env: 'FXA_OPENID_KEY'
|
||||
},
|
||||
oldKey: {
|
||||
doc: 'The previous public key that was used to sign id_tokens',
|
||||
default: {},
|
||||
env: 'FXA_OPENID_OLDKEY'
|
||||
},
|
||||
issuer: {
|
||||
// this should match `issuer` in the 'OpenID Provider Metadata' document
|
||||
// from the fxa-content-server
|
||||
doc: 'The value of the `iss` property of the id_token',
|
||||
default: 'https://accounts.firefox.com',
|
||||
env: 'FXA_OPENID_ISSUER'
|
||||
},
|
||||
ttl: {
|
||||
doc: 'Number of milliseconds until id_token should expire',
|
||||
default: '5 minutes',
|
||||
format: 'duration',
|
||||
env: 'FXA_OPENID_TTL'
|
||||
}
|
||||
},
|
||||
publicUrl: {
|
||||
format: 'url',
|
||||
default: 'http://127.0.0.1:9010',
|
||||
env: 'PUBLIC_URL'
|
||||
},
|
||||
server: {
|
||||
host: {
|
||||
env: 'HOST',
|
||||
default: '127.0.0.1'
|
||||
},
|
||||
port: {
|
||||
env: 'PORT',
|
||||
format: 'port',
|
||||
default: 9010
|
||||
}
|
||||
},
|
||||
serverInternal: {
|
||||
host: {
|
||||
env: 'HOST_INTERNAL',
|
||||
default: '127.0.0.1'
|
||||
},
|
||||
port: {
|
||||
env: 'PORT_INTERNAL',
|
||||
format: 'port',
|
||||
default: 9011
|
||||
}
|
||||
},
|
||||
serviceClients: {
|
||||
doc: 'Clients that can make oauth requests for any user',
|
||||
default: []
|
||||
},
|
||||
i18n: {
|
||||
defaultLanguage: {
|
||||
format: String,
|
||||
default: 'en',
|
||||
env: 'DEFAULT_LANG'
|
||||
},
|
||||
supportedLanguages: {
|
||||
format: Array,
|
||||
default: DEFAULT_SUPPORTED_LANGUAGES,
|
||||
env: 'SUPPORTED_LANGS'
|
||||
}
|
||||
},
|
||||
unique: {
|
||||
clientSecret: {
|
||||
doc: 'Bytes of generated client_secrets',
|
||||
default: 32
|
||||
},
|
||||
code: {
|
||||
doc: 'Bytes of generated codes',
|
||||
default: 32
|
||||
},
|
||||
id: {
|
||||
doc: 'Bytes of generated DB ids',
|
||||
default: 8
|
||||
},
|
||||
token: {
|
||||
doc: 'Bytes of generated tokens',
|
||||
default: 32
|
||||
},
|
||||
developerId: {
|
||||
doc: 'Bytes of generated developer ids',
|
||||
default: 16
|
||||
}
|
||||
},
|
||||
cacheControl: {
|
||||
doc: 'Hapi: a string with the value of the "Cache-Control" header when caching is disabled',
|
||||
format: String,
|
||||
default: 'private, no-cache, no-store, must-revalidate'
|
||||
},
|
||||
sentryDsn: {
|
||||
doc: 'Sentry DSN for error and log reporting',
|
||||
default: '',
|
||||
format: 'String',
|
||||
env: 'SENTRY_DSN'
|
||||
}
|
||||
});
|
||||
|
||||
var envConfig = path.join(__dirname, '..', 'config', conf.get('env') + '.json');
|
||||
var files = (envConfig + ',' + process.env.CONFIG_FILES)
|
||||
.split(',').filter(fs.existsSync);
|
||||
conf.loadFile(files);
|
||||
|
||||
var options = {
|
||||
allowed: 'strict'
|
||||
};
|
||||
|
||||
conf.validate(options);
|
||||
|
||||
// custom validation, since we cant yet specify rules for inside arrays
|
||||
conf.get('serviceClients').forEach(function(client) {
|
||||
assert(client.id, 'client id required');
|
||||
assert.equal(client.id.length, 16, 'client id must be 16 hex digits');
|
||||
assert.equal(Buffer.from(client.id, 'hex').toString('hex'), client.id,
|
||||
'client id must be 16 hex digits');
|
||||
assert.equal(typeof client.name, 'string', 'client name required');
|
||||
assert.equal(typeof client.scope, 'string', 'client scope required');
|
||||
assert.equal(typeof client.jku, 'string', 'client jku required');
|
||||
});
|
||||
|
||||
// Replace openid key if file specified
|
||||
if (conf.get('openid.keyFile')){
|
||||
conf.set('openid.key', require(conf.get('openid.keyFile')));
|
||||
}
|
||||
|
||||
if (conf.get('openid.oldKeyFile')){
|
||||
conf.set('openid.oldKey', require(conf.get('openid.oldKeyFile')));
|
||||
}
|
||||
|
||||
var key = conf.get('openid.key');
|
||||
assert.equal(key.kty, 'RSA', 'openid.key.kty must be RSA');
|
||||
assert(key.kid, 'openid.key.kid is required');
|
||||
assert(key.n, 'openid.key.n is required');
|
||||
assert(key.e, 'openid.key.e is required');
|
||||
assert(key.d, 'openid.key.d is required');
|
||||
|
||||
var oldKey = conf.get('openid.oldKey');
|
||||
if (Object.keys(oldKey).length) {
|
||||
assert.equal(oldKey.kty, 'RSA', 'openid.oldKey.kty must be RSA');
|
||||
assert(oldKey.kid, 'openid.oldKey.kid is required');
|
||||
assert.notEqual(key.kid, oldKey.kid,
|
||||
'openid.key.kid must differ from oldKey');
|
||||
assert(oldKey.n, 'openid.oldKey.n is required');
|
||||
assert(oldKey.e, 'openid.oldKey.e is required');
|
||||
assert(! oldKey.d, 'openid.oldKey.d is forbidden');
|
||||
}
|
||||
|
||||
module.exports = conf;
|
|
@ -1,85 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/***
|
||||
* This module consists of helpers that are used by both memory and MySQL database engines.
|
||||
**/
|
||||
|
||||
const unbuf = require('buf').unbuf.hex;
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* This helper takes a list of active oauth tokens and produces an aggregate
|
||||
* summary of the active clients, by:
|
||||
*
|
||||
* * merging all scopes into a single set
|
||||
* * taking the max token creation time a the last access time
|
||||
*
|
||||
* The resulting array is in sorted order by last access time, then client name.
|
||||
*
|
||||
* @param {Array} activeClientTokens
|
||||
* An array of OAuth tokens, annotated with client name:
|
||||
* (OAuth client) name|(OAuth client) id|(Token) createdAt|(Token) scope
|
||||
* @returns {Array}
|
||||
*/
|
||||
aggregateActiveClients: function aggregateActiveClients(activeClientTokens) {
|
||||
if (! activeClientTokens) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Create an Object that stores unique OAuth client data
|
||||
var activeClients = {};
|
||||
activeClientTokens.forEach(function (clientTokenObj) {
|
||||
var clientIdHex = unbuf(clientTokenObj.id);
|
||||
|
||||
if (! activeClients[clientIdHex]) {
|
||||
// add the OAuth client if not already in the Object
|
||||
activeClients[clientIdHex] = {
|
||||
id: clientTokenObj.id,
|
||||
name: clientTokenObj.name,
|
||||
lastAccessTime: clientTokenObj.createdAt,
|
||||
scope: ScopeSet.fromArray([])
|
||||
};
|
||||
}
|
||||
|
||||
activeClients[clientIdHex].scope.add(clientTokenObj.scope);
|
||||
|
||||
var clientTokenTime = clientTokenObj.createdAt;
|
||||
if (clientTokenTime > activeClients[clientIdHex].lastAccessTime) {
|
||||
// only update the createdAt if it is newer
|
||||
activeClients[clientIdHex].lastAccessTime = clientTokenTime;
|
||||
}
|
||||
});
|
||||
|
||||
// Sort the scopes alphabetically, convert the Object structure to an array
|
||||
var activeClientsArray = Object.keys(activeClients).map(function (key) {
|
||||
activeClients[key].scope = activeClients[key].scope.getScopeValues().sort();
|
||||
return activeClients[key];
|
||||
});
|
||||
|
||||
// Sort the final Array structure first by lastAccessTime and then name
|
||||
activeClientsArray.sort(function(a, b) {
|
||||
if (b.lastAccessTime > a.lastAccessTime) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (b.lastAccessTime < a.lastAccessTime) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return activeClientsArray;
|
||||
}
|
||||
};
|
|
@ -1,214 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const unbuf = require('buf').unbuf.hex;
|
||||
|
||||
const P = require('../promise');
|
||||
|
||||
const config = require('../config');
|
||||
const encrypt = require('../encrypt');
|
||||
const logger = require('../logging')('db');
|
||||
const klass = config.get('db.driver') === 'mysql' ?
|
||||
require('./mysql') : require('./memory');
|
||||
const unique = require('../unique');
|
||||
|
||||
function clientEquals(configClient, dbClient) {
|
||||
var props = Object.keys(configClient);
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var prop = props[i];
|
||||
var configProp = unbuf(configClient[prop]);
|
||||
var dbProp = unbuf(dbClient[prop]);
|
||||
if (configProp !== dbProp) {
|
||||
logger.debug('clients.differ', {
|
||||
prop: prop,
|
||||
configProp: configProp,
|
||||
dbProp: dbProp
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function convertClientToConfigFormat(client) {
|
||||
var out = {};
|
||||
|
||||
for (var key in client) {
|
||||
if (key === 'hashedSecret' || key === 'hashedSecretPrevious') {
|
||||
out[key] = unbuf(client[key]);
|
||||
} else if (key === 'trusted' || key === 'canGrant') {
|
||||
out[key] = !! client[key]; // db stores booleans as 0 or 1.
|
||||
} else if (typeof client[key] !== 'function') {
|
||||
out[key] = unbuf(client[key]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function preClients() {
|
||||
var clients = config.get('clients');
|
||||
if (clients && clients.length) {
|
||||
logger.debug('predefined.loading', { clients: clients });
|
||||
return P.all(clients.map(function(c) {
|
||||
if (c.secret) {
|
||||
console.error('Do not keep client secrets in the config file.' // eslint-disable-line no-console
|
||||
+ ' Use the `hashedSecret` field instead.\n\n'
|
||||
+ '\tclient=%s has `secret` field\n'
|
||||
+ '\tuse hashedSecret="%s" instead',
|
||||
c.id,
|
||||
unbuf(encrypt.hash(c.secret)));
|
||||
return P.reject(new Error('Do not keep client secrets in the config file.'));
|
||||
}
|
||||
|
||||
// ensure the required keys are present.
|
||||
var err = null;
|
||||
var REQUIRED_CLIENTS_KEYS = [ 'id', 'hashedSecret', 'name', 'imageUri',
|
||||
'redirectUri', 'trusted', 'canGrant' ];
|
||||
REQUIRED_CLIENTS_KEYS.forEach(function(key) {
|
||||
if (! (key in c)) {
|
||||
var data = { key: key, name: c.name || 'unknown' };
|
||||
logger.error('client.missing.keys', data);
|
||||
err = new Error('Client config has missing keys');
|
||||
}
|
||||
});
|
||||
if (err) {
|
||||
return P.reject(err);
|
||||
}
|
||||
|
||||
// ensure booleans are boolean and not undefined
|
||||
c.trusted = !! c.trusted;
|
||||
c.canGrant = !! c.canGrant;
|
||||
c.publicClient = !! c.publicClient;
|
||||
|
||||
// Modification of the database at startup in production and stage is
|
||||
// not preferred. This option will be set to false on those stacks.
|
||||
if (! config.get('db.autoUpdateClients')) {
|
||||
return P.resolve();
|
||||
}
|
||||
|
||||
return exports.getClient(c.id).then(function(client) {
|
||||
if (client) {
|
||||
client = convertClientToConfigFormat(client);
|
||||
logger.info('client.compare', { id: c.id });
|
||||
if (clientEquals(client, c)) {
|
||||
logger.info('client.compare.equal', { id: c.id });
|
||||
} else {
|
||||
logger.warn('client.compare.differs', {
|
||||
id: c.id,
|
||||
before: client,
|
||||
after: c
|
||||
});
|
||||
return exports.updateClient(c);
|
||||
}
|
||||
} else {
|
||||
return exports.registerClient(c);
|
||||
}
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
return P.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function serviceClients() {
|
||||
var clients = config.get('serviceClients');
|
||||
if (clients && clients.length) {
|
||||
logger.debug('serviceClients.loading', clients);
|
||||
|
||||
return P.all(clients.map(function(client) {
|
||||
return exports.getClient(client.id).then(function(existing) {
|
||||
if (existing) {
|
||||
logger.verbose('seviceClients.existing', client);
|
||||
return;
|
||||
}
|
||||
|
||||
return exports.registerClient({
|
||||
id: client.id,
|
||||
name: client.name,
|
||||
hashedSecret: encrypt.hash(unique.secret()),
|
||||
imageUri: '',
|
||||
redirectUri: '',
|
||||
trusted: true,
|
||||
canGrant: false
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert pre-defined list of scopes into the DB
|
||||
*/
|
||||
function scopes() {
|
||||
var scopes = config.get('scopes');
|
||||
if (scopes && scopes.length) {
|
||||
logger.debug('scopes.loading', JSON.stringify(scopes));
|
||||
|
||||
return P.all(scopes.map(function(s) {
|
||||
return exports.getScope(s.scope).then(function(existing) {
|
||||
if (existing) {
|
||||
logger.verbose('scopes.existing', s);
|
||||
return;
|
||||
}
|
||||
|
||||
return exports.registerScope(s);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
var driver;
|
||||
function withDriver() {
|
||||
if (driver) {
|
||||
return P.resolve(driver);
|
||||
}
|
||||
var p;
|
||||
if (config.get('db.driver') === 'mysql') {
|
||||
p = klass.connect(config.get('mysql'));
|
||||
} else {
|
||||
p = klass.connect();
|
||||
}
|
||||
return p.then(function(store) {
|
||||
logger.debug('connected', { driver: config.get('db.driver') });
|
||||
driver = store;
|
||||
}).then(exports._initialClients).then(function() {
|
||||
return driver;
|
||||
});
|
||||
}
|
||||
|
||||
const proxyReturn = logger.isEnabledFor(logger.VERBOSE) ?
|
||||
function verboseReturn(promise, method) {
|
||||
return promise.then(function(ret) {
|
||||
logger.verbose('proxied', { method: method, ret: ret });
|
||||
return ret;
|
||||
});
|
||||
} : function identity(x) {
|
||||
return x;
|
||||
};
|
||||
|
||||
function proxy(method) {
|
||||
return function proxied() {
|
||||
var args = arguments;
|
||||
return withDriver().then(function(driver) {
|
||||
logger.verbose('proxying', { method: method, args: args });
|
||||
return proxyReturn(driver[method].apply(driver, args), method);
|
||||
}).catch(function(err) {
|
||||
logger.error(method, err);
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Object.keys(klass.prototype).forEach(function(key) {
|
||||
exports[key] = proxy(key);
|
||||
});
|
||||
|
||||
exports.disconnect = function disconnect() {
|
||||
driver = null;
|
||||
};
|
||||
|
||||
exports._initialClients = function() {
|
||||
return preClients().then(serviceClients).then(scopes);
|
||||
};
|
|
@ -1,518 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const buf = require('buf').hex;
|
||||
const unbuf = require('buf').unbuf.hex;
|
||||
|
||||
const config = require('../config');
|
||||
const encrypt = require('../encrypt');
|
||||
const helpers = require('./helpers');
|
||||
const logger = require('../logging')('db.memory');
|
||||
const P = require('../promise');
|
||||
const unique = require('../unique');
|
||||
|
||||
const MAX_TTL = config.get('expiration.accessToken');
|
||||
|
||||
/*
|
||||
* MemoryStore structure:
|
||||
* MemoryStore = {
|
||||
* clients: {
|
||||
* <id>: {
|
||||
* id: <id>,
|
||||
* hashedSecret: <string>,
|
||||
* hashedSecretPrevious: <string>,
|
||||
* name: <string>,
|
||||
* imageUri: <string>,
|
||||
* redirectUri: <string>,
|
||||
* trusted: <boolean>,
|
||||
* createdAt: <timestamp>
|
||||
* }
|
||||
* },
|
||||
* codes: {
|
||||
* <code>: {
|
||||
* clientId: <client_id>,
|
||||
* userId: <user_id>,
|
||||
* code: <string>
|
||||
* scope: <string>,
|
||||
* authAt: <timestamp>,
|
||||
* amr: [<string>]
|
||||
* aal: <integer>,
|
||||
* createdAt: <timestamp>,
|
||||
* offline: <boolean>,
|
||||
* codeChallengeMethod: <string>,
|
||||
* codeChallenge: <string>,
|
||||
* }
|
||||
* },
|
||||
* developers: {
|
||||
* <developerId>: {
|
||||
* developerId: <developer_id>,
|
||||
* email: <string>,
|
||||
* createdAt: <timestamp>
|
||||
* }
|
||||
* },
|
||||
* clientDevelopers: {
|
||||
* <id>: {
|
||||
* developerId: <developer_id>,
|
||||
* clientId: <client_id>,
|
||||
* createdAt: <timestamp>
|
||||
* }
|
||||
* },
|
||||
* tokens: {
|
||||
* <token>: {
|
||||
* token: <string>,
|
||||
* clientId: <client_id>,
|
||||
* userId: <user_id>,
|
||||
* type: <string>,
|
||||
* scope: <string>,
|
||||
* createdAt: <timestamp>,
|
||||
* expiresAt: <timestamp>
|
||||
* }
|
||||
* },
|
||||
* refreshTokens: {
|
||||
* <token>: {
|
||||
* token: <string>,
|
||||
* clientId: <client_id>,
|
||||
* userId: <user_id>,
|
||||
* scope: <string>,
|
||||
* createdAt: <timestamp>,
|
||||
* lastUsedAt: <timestamp>
|
||||
* }
|
||||
* },
|
||||
* scopes: {
|
||||
* <scope>: {
|
||||
* scope: <scope>,
|
||||
* hasScopedKeys: <hasScopedKeys>
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
function MemoryStore() {
|
||||
if (! (this instanceof MemoryStore)) {
|
||||
return new MemoryStore();
|
||||
}
|
||||
this.clients = {};
|
||||
this.codes = {};
|
||||
this.tokens = {};
|
||||
this.developers = {};
|
||||
this.clientDevelopers = {};
|
||||
this.refreshTokens = {};
|
||||
this.scopes = {};
|
||||
}
|
||||
|
||||
MemoryStore.connect = function memoryConnect() {
|
||||
return P.resolve(new MemoryStore());
|
||||
};
|
||||
|
||||
function clone(obj) {
|
||||
if (! obj) {
|
||||
return obj;
|
||||
}
|
||||
var clone = {};
|
||||
for (var k in obj) {
|
||||
clone[k] = obj[k];
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
function deleteByUserId(object, userId) {
|
||||
var ids = Object.keys(object);
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
var id = ids[i];
|
||||
if (object[id].userId === userId) {
|
||||
delete object[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deleteByClientId(object, clientId) {
|
||||
var ids = Object.keys(object);
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
var id = ids[i];
|
||||
if (unbuf(object[id].clientId) === clientId) {
|
||||
delete object[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MemoryStore.prototype = {
|
||||
ping: function ping() {
|
||||
return P.resolve({});
|
||||
},
|
||||
registerClient: function registerClient(client) {
|
||||
if (client.id) {
|
||||
client.id = buf(client.id);
|
||||
} else {
|
||||
client.id = unique.id();
|
||||
}
|
||||
var hex = unbuf(client.id);
|
||||
logger.debug('registerClient', { name: client.name, id: hex });
|
||||
client.createdAt = new Date();
|
||||
client.imageUri = client.imageUri || '';
|
||||
client.redirectUri = client.redirectUri || '';
|
||||
client.canGrant = !! client.canGrant;
|
||||
client.trusted = !! client.trusted;
|
||||
this.clients[hex] = client;
|
||||
client.hashedSecret = client.hashedSecret;
|
||||
client.hashedSecretPrevious = client.hashedSecretPrevious || '';
|
||||
return P.resolve(client);
|
||||
},
|
||||
updateClient: function updateClient(client) {
|
||||
if (! client.id) {
|
||||
return P.reject(new Error('Update client needs an id'));
|
||||
}
|
||||
var hex = unbuf(client.id);
|
||||
if (! this.clients[hex]) {
|
||||
return P.reject(new Error('Client does not exist'));
|
||||
}
|
||||
var old = this.clients[hex];
|
||||
for (var key in client) {
|
||||
if (key === 'id') {
|
||||
// nothing
|
||||
} else if (key === 'hashedSecret') {
|
||||
old.hashedSecret = buf(client[key]);
|
||||
} else if (key === 'hashedSecretPrevious') {
|
||||
old.hashedSecretPrevious = buf(client[key]);
|
||||
} else if (client[key] !== undefined) {
|
||||
old[key] = client[key];
|
||||
}
|
||||
}
|
||||
return P.resolve(old);
|
||||
},
|
||||
getClient: function getClient(id) {
|
||||
return P.resolve(this.clients[unbuf(id)]);
|
||||
},
|
||||
getClients: function getClients(email) {
|
||||
var self = this;
|
||||
|
||||
return this.getDeveloper(email)
|
||||
.then(function (developer) {
|
||||
if (! developer) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var clients = [];
|
||||
|
||||
Object.keys(self.clientDevelopers).forEach(function(key) {
|
||||
var entry = self.clientDevelopers[key];
|
||||
if (entry.developerId === developer.developerId) {
|
||||
clients.push(unbuf(entry.clientId));
|
||||
}
|
||||
});
|
||||
|
||||
return clients.map(function(id) {
|
||||
var client = self.clients[id];
|
||||
|
||||
return {
|
||||
id: client.id,
|
||||
name: client.name,
|
||||
imageUri: client.imageUri,
|
||||
redirectUri: client.redirectUri,
|
||||
canGrant: client.canGrant,
|
||||
trusted: client.trusted
|
||||
};
|
||||
}, this);
|
||||
});
|
||||
},
|
||||
removeClient: function removeClient(id) {
|
||||
delete this.clients[unbuf(id)];
|
||||
return P.resolve();
|
||||
},
|
||||
generateCode: function generateCode(codeObj) {
|
||||
codeObj = clone(codeObj);
|
||||
codeObj.createdAt = new Date();
|
||||
var code = unique.code();
|
||||
codeObj.code = encrypt.hash(code);
|
||||
this.codes[unbuf(codeObj.code)] = codeObj;
|
||||
return P.resolve(code);
|
||||
},
|
||||
getCode: function getCode(code) {
|
||||
return P.resolve(clone(this.codes[unbuf(encrypt.hash(code))]));
|
||||
},
|
||||
removeCode: function removeCode(code) {
|
||||
delete this.codes[unbuf(encrypt.hash(code))];
|
||||
return P.resolve();
|
||||
},
|
||||
generateAccessToken: function generateAccessToken(vals) {
|
||||
var token = unique.token();
|
||||
var now = new Date();
|
||||
var t = {
|
||||
clientId: vals.clientId,
|
||||
userId: vals.userId,
|
||||
email: vals.email,
|
||||
scope: vals.scope,
|
||||
type: 'bearer',
|
||||
createdAt: now,
|
||||
// ttl is in seconds
|
||||
expiresAt: new Date(+now + (vals.ttl * 1000 || MAX_TTL)),
|
||||
token: encrypt.hash(token),
|
||||
profileChangedAt: vals.profileChangedAt || 0
|
||||
};
|
||||
var ret = clone(t);
|
||||
this.tokens[unbuf(t.token)] = t;
|
||||
ret.token = token;
|
||||
return P.resolve(ret);
|
||||
},
|
||||
getAccessToken: function getAccessToken(token) {
|
||||
return P.resolve(clone(this.tokens[unbuf(token)]));
|
||||
},
|
||||
removeAccessToken: function removeAccessToken(id) {
|
||||
delete this.tokens[unbuf(id)];
|
||||
return P.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all services that have have non-expired tokens
|
||||
* @param {String} uid User ID as hex
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getActiveClientsByUid: function getActiveClientsByUid(uid) {
|
||||
var self = this;
|
||||
if (! uid) {
|
||||
return P.reject(new Error('Uid is required'));
|
||||
}
|
||||
|
||||
var activeClientTokens = [];
|
||||
var ids = Object.keys(this.tokens);
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
var id = ids[i];
|
||||
if (this.tokens[id].userId.toString('hex') === uid) {
|
||||
var clientIdHex = unbuf(this.tokens[id].clientId);
|
||||
var client = self.clients[clientIdHex];
|
||||
if (client.canGrant === false) {
|
||||
activeClientTokens.push({
|
||||
id: this.tokens[id].clientId,
|
||||
createdAt: this.tokens[id].createdAt,
|
||||
name: client.name,
|
||||
scope: this.tokens[id].scope
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return P.resolve(helpers.aggregateActiveClients(activeClientTokens));
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete all authorization grants for some clientId and uid.
|
||||
*
|
||||
* @param {String} clientId Client ID
|
||||
* @param {String} uid User Id as Hex
|
||||
e @returns {Promise}
|
||||
*/
|
||||
deleteClientAuthorization: function deleteClientAuthorization(clientId, uid) {
|
||||
if (! clientId || ! uid) {
|
||||
return P.reject(new Error('clientId and uid are required'));
|
||||
}
|
||||
|
||||
function deleteToken(tokens) {
|
||||
const ids = Object.keys(tokens);
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
if (tokens[id].userId.toString('hex') === uid &&
|
||||
tokens[id].clientId.toString('hex') === clientId) {
|
||||
delete tokens[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteToken(this.codes);
|
||||
deleteToken(this.tokens);
|
||||
deleteToken(this.refreshTokens);
|
||||
|
||||
return P.resolve({});
|
||||
},
|
||||
generateRefreshToken: function generateRefreshToken(vals) {
|
||||
var token = unique.token();
|
||||
var t = {
|
||||
clientId: vals.clientId,
|
||||
userId: vals.userId,
|
||||
email: vals.email,
|
||||
scope: vals.scope,
|
||||
createdAt: new Date(),
|
||||
lastUsedAt: new Date(),
|
||||
token: encrypt.hash(token),
|
||||
profileChangedAt: vals.profileChangedAt
|
||||
};
|
||||
var ret = clone(t);
|
||||
this.refreshTokens[unbuf(t.token)] = t;
|
||||
ret.token = token;
|
||||
return P.resolve(ret);
|
||||
},
|
||||
getRefreshToken: function getRefreshToken(token) {
|
||||
return P.resolve(clone(this.refreshTokens[unbuf(token)]));
|
||||
},
|
||||
usedRefreshToken: function usedRefreshToken(token) {
|
||||
if (! token) {
|
||||
return P.reject(new Error('Update needs a token'));
|
||||
}
|
||||
var hex = unbuf(token);
|
||||
if (! this.refreshTokens[hex]) {
|
||||
return P.reject(new Error('Token does not exist'));
|
||||
}
|
||||
var old = this.refreshTokens[hex];
|
||||
old.lastUsedAt = new Date();
|
||||
|
||||
return P.resolve(old);
|
||||
},
|
||||
|
||||
removeRefreshToken: function removeRefreshToken(id) {
|
||||
delete this.refreshTokens[unbuf(id)];
|
||||
return P.resolve();
|
||||
},
|
||||
getEncodingInfo: function getEncodingInfo() {
|
||||
console.warn('getEncodingInfo has no meaning with memory implementation'); // eslint-disable-line no-console
|
||||
return P.resolve({});
|
||||
},
|
||||
removeUser: function removeUser(userId) {
|
||||
deleteByUserId(this.tokens, userId);
|
||||
deleteByUserId(this.refreshTokens, userId);
|
||||
deleteByUserId(this.codes, userId);
|
||||
return P.resolve();
|
||||
},
|
||||
/**
|
||||
* Removes user's tokens and refreshTokens for canGrant and publicClient clients
|
||||
*
|
||||
* @param userId
|
||||
* @returns {Promise}
|
||||
*/
|
||||
removePublicAndCanGrantTokens: function removePublicAndCanGrantTokens(userId) {
|
||||
Object.keys(this.clients).forEach((clientId) => {
|
||||
const client = this.clients[clientId];
|
||||
if (client.publicClient || client.canGrant) {
|
||||
deleteByClientId(this.tokens, clientId);
|
||||
deleteByClientId(this.refreshTokens, clientId);
|
||||
}
|
||||
});
|
||||
return P.resolve();
|
||||
},
|
||||
getScope: function getScope (scope) {
|
||||
return P.resolve(this.scopes[scope]);
|
||||
},
|
||||
registerScope: function registerScope (scope) {
|
||||
this.scopes[scope.scope] = scope;
|
||||
return P.resolve();
|
||||
},
|
||||
activateDeveloper: function activateDeveloper(email) {
|
||||
var self = this;
|
||||
|
||||
if (! email) {
|
||||
return P.reject(new Error('Email is required'));
|
||||
}
|
||||
|
||||
return this.getDeveloper(email)
|
||||
.then(function(result) {
|
||||
if (result) {
|
||||
return P.reject(new Error('ER_DUP_ENTRY'));
|
||||
}
|
||||
|
||||
var newId = unique.developerId();
|
||||
var developer = {
|
||||
developerId: newId,
|
||||
email: email,
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
self.developers[unbuf(newId)] = developer;
|
||||
return developer;
|
||||
|
||||
});
|
||||
},
|
||||
getDeveloper: function getDeveloper(email) {
|
||||
var self = this;
|
||||
var developer = null;
|
||||
|
||||
if (! email) {
|
||||
return P.reject(new Error('Email is required'));
|
||||
}
|
||||
|
||||
Object.keys(self.developers).forEach(function(developerId) {
|
||||
var devEntry = self.developers[developerId];
|
||||
|
||||
if (devEntry.email === email) {
|
||||
developer = devEntry;
|
||||
}
|
||||
});
|
||||
|
||||
return P.resolve(developer);
|
||||
},
|
||||
removeDeveloper: function removeDeveloper(email) {
|
||||
var self = this;
|
||||
|
||||
if (! email) {
|
||||
return P.reject(new Error('Email is required'));
|
||||
}
|
||||
|
||||
Object.keys(self.developers).forEach(function(developerId) {
|
||||
var devEntry = self.developers[developerId];
|
||||
|
||||
if (devEntry.email === email) {
|
||||
delete self.developers[developerId];
|
||||
}
|
||||
});
|
||||
|
||||
return P.resolve();
|
||||
},
|
||||
developerOwnsClient: function devOwnsClient(developerEmail, clientId) {
|
||||
var self = this;
|
||||
var developerId;
|
||||
|
||||
logger.debug('developerOwnsClient');
|
||||
return self.getDeveloper(developerEmail)
|
||||
.then(function (developer) {
|
||||
if (! developer) {
|
||||
return P.reject();
|
||||
}
|
||||
developerId = developer.developerId;
|
||||
|
||||
return self.getClientDevelopers(clientId);
|
||||
})
|
||||
.then(function (developers) {
|
||||
|
||||
function hasDeveloper(developer) {
|
||||
return unbuf(developer.developerId) === unbuf(developerId);
|
||||
}
|
||||
|
||||
if (developers.some(hasDeveloper)) {
|
||||
return P.resolve(true);
|
||||
} else {
|
||||
return P.reject(false);
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
registerClientDeveloper: function regClientDeveloper(developerId, clientId) {
|
||||
var entry = {
|
||||
developerId: buf(developerId),
|
||||
clientId: buf(clientId),
|
||||
createdAt: new Date()
|
||||
};
|
||||
var uniqueHexId = unbuf(unique.id());
|
||||
|
||||
logger.debug('registerClientDeveloper', entry);
|
||||
this.clientDevelopers[uniqueHexId] = entry;
|
||||
return P.resolve(entry);
|
||||
},
|
||||
getClientDevelopers: function getClientDevelopers(clientId) {
|
||||
var self = this;
|
||||
var developers = [];
|
||||
|
||||
if (! clientId) {
|
||||
return P.reject(new Error('Client id is required'));
|
||||
}
|
||||
|
||||
clientId = unbuf(clientId);
|
||||
|
||||
Object.keys(self.clientDevelopers).forEach(function(key) {
|
||||
var entry = self.clientDevelopers[key];
|
||||
|
||||
if (unbuf(entry.clientId) === clientId) {
|
||||
developers.push(self.developers[unbuf(entry.developerId)]);
|
||||
}
|
||||
});
|
||||
|
||||
return P.resolve(developers);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = MemoryStore;
|
|
@ -1,903 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const buf = require('buf').hex;
|
||||
const hex = require('buf').to.hex;
|
||||
const moment = require('moment');
|
||||
const mysql = require('mysql');
|
||||
const MysqlPatcher = require('mysql-patcher');
|
||||
|
||||
const config = require('../../config');
|
||||
const encrypt = require('../../encrypt');
|
||||
const helpers = require('../helpers');
|
||||
const P = require('../../promise');
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
const unique = require('../../unique');
|
||||
const patch = require('./patch');
|
||||
|
||||
const MAX_TTL = config.get('expiration.accessToken');
|
||||
const REQUIRED_SQL_MODES = [
|
||||
'STRICT_ALL_TABLES',
|
||||
'NO_ENGINE_SUBSTITUTION'
|
||||
];
|
||||
const REQUIRED_CHARSET = 'UTF8MB4_UNICODE_CI';
|
||||
|
||||
// logger is not const to support mocking in the unit tests
|
||||
var logger = require('../../logging')('db.mysql');
|
||||
|
||||
function MysqlStore(options) {
|
||||
if (options.charset && options.charset !== REQUIRED_CHARSET) {
|
||||
logger.warn('createDatabase.invalidCharset', { charset: options.charset });
|
||||
throw new Error('You cannot use any charset besides ' + REQUIRED_CHARSET);
|
||||
} else {
|
||||
options.charset = REQUIRED_CHARSET;
|
||||
}
|
||||
options.typeCast = function(field, next) {
|
||||
if (field.type === 'TINY' && field.length === 1) {
|
||||
return field.string() === '1';
|
||||
}
|
||||
return next();
|
||||
};
|
||||
logger.info('pool.create', { options: options });
|
||||
var pool = this._pool = mysql.createPool(options);
|
||||
pool.on('enqueue', function() {
|
||||
logger.info('pool.enqueue', {
|
||||
queueLength: pool._connectionQueue && pool._connectionQueue.length
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Apply patches up to the current patch level.
|
||||
// This will also create the DB if it is missing.
|
||||
|
||||
function updateDbSchema(patcher) {
|
||||
logger.verbose('updateDbSchema', patcher.options);
|
||||
|
||||
var d = P.defer();
|
||||
patcher.patch(function(err) {
|
||||
if (err) {
|
||||
logger.error('updateDbSchema', err);
|
||||
return d.reject(err);
|
||||
}
|
||||
d.resolve();
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
// Sanity-check that we're working with a compatible patch level.
|
||||
|
||||
function checkDbPatchLevel(patcher) {
|
||||
logger.verbose('checkDbPatchLevel', patcher.options);
|
||||
|
||||
var d = P.defer();
|
||||
|
||||
patcher.readDbPatchLevel(function(err) {
|
||||
if (err) {
|
||||
return d.reject(err);
|
||||
}
|
||||
|
||||
// We can run if we're at or above some patch level. Should be
|
||||
// equal at initial deployment, and may be one or more higher
|
||||
// later on, due to database changes in preparation for the next
|
||||
// release.
|
||||
if (patcher.currentPatchLevel >= patch.level) {
|
||||
return d.resolve();
|
||||
}
|
||||
|
||||
err = 'unexpected db patch level: ' + patcher.currentPatchLevel;
|
||||
return d.reject(new Error(err));
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
MysqlStore.connect = function mysqlConnect(options) {
|
||||
if (options.logger) {
|
||||
logger = options.logger;
|
||||
}
|
||||
|
||||
options.createDatabase = options.createSchema;
|
||||
options.dir = path.join(__dirname, 'patches');
|
||||
options.metaTable = 'dbMetadata';
|
||||
options.patchKey = 'schema-patch-level';
|
||||
options.patchLevel = patch.level;
|
||||
options.mysql = mysql;
|
||||
var patcher = new MysqlPatcher(options);
|
||||
|
||||
return P.promisify(patcher.connect, {context: patcher})().then(function() {
|
||||
if (options.createSchema) {
|
||||
return updateDbSchema(patcher);
|
||||
}
|
||||
}).then(function() {
|
||||
return checkDbPatchLevel(patcher);
|
||||
}).catch(function(error) {
|
||||
logger.error('checkDbPatchLevel', error);
|
||||
throw error;
|
||||
}).finally(function () {
|
||||
return P.promisify(patcher.end, {context: patcher})();
|
||||
}).then(function() {
|
||||
return new MysqlStore(options);
|
||||
});
|
||||
};
|
||||
|
||||
const QUERY_CLIENT_REGISTER =
|
||||
'INSERT INTO clients ' +
|
||||
'(id, name, imageUri, hashedSecret, hashedSecretPrevious, redirectUri,' +
|
||||
'trusted, allowedScopes, canGrant, publicClient) ' +
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);';
|
||||
const QUERY_CLIENT_DEVELOPER_INSERT =
|
||||
'INSERT INTO clientDevelopers ' +
|
||||
'(rowId, developerId, clientId) ' +
|
||||
'VALUES (?, ?, ?);';
|
||||
const QUERY_CLIENT_DEVELOPER_LIST_BY_CLIENT_ID =
|
||||
'SELECT developers.email, developers.createdAt ' +
|
||||
'FROM clientDevelopers, developers ' +
|
||||
'WHERE clientDevelopers.developerId = developers.developerId ' +
|
||||
'AND clientDevelopers.clientId=?;';
|
||||
const QUERY_DEVELOPER_OWNS_CLIENT =
|
||||
'SELECT clientDevelopers.rowId ' +
|
||||
'FROM clientDevelopers, developers ' +
|
||||
'WHERE developers.developerId = clientDevelopers.developerId ' +
|
||||
'AND developers.email =? AND clientDevelopers.clientId =?;';
|
||||
const QUERY_DEVELOPER_INSERT =
|
||||
'INSERT INTO developers ' +
|
||||
'(developerId, email) ' +
|
||||
'VALUES (?, ?);';
|
||||
const QUERY_CLIENT_GET = 'SELECT * FROM clients WHERE id=?';
|
||||
const QUERY_CLIENT_LIST = 'SELECT id, name, redirectUri, imageUri, ' +
|
||||
'canGrant, publicClient, trusted ' +
|
||||
'FROM clients, clientDevelopers, developers ' +
|
||||
'WHERE clients.id = clientDevelopers.clientId AND ' +
|
||||
'developers.developerId = clientDevelopers.developerId AND ' +
|
||||
'developers.email =?;';
|
||||
const QUERY_PUBLIC_CLIENTS_LIST = 'SELECT * FROM clients WHERE publicClient = 1 OR canGrant = 1;';
|
||||
const QUERY_CLIENT_UPDATE = 'UPDATE clients SET ' +
|
||||
'name=COALESCE(?, name), imageUri=COALESCE(?, imageUri), ' +
|
||||
'hashedSecret=COALESCE(?, hashedSecret), ' +
|
||||
'hashedSecretPrevious=COALESCE(?, hashedSecretPrevious), ' +
|
||||
'redirectUri=COALESCE(?, redirectUri), ' +
|
||||
'trusted=COALESCE(?, trusted), allowedScopes=COALESCE(?, allowedScopes), ' +
|
||||
'canGrant=COALESCE(?, canGrant) ' +
|
||||
'WHERE id=?';
|
||||
// This query deletes everythin related to the client, and is thus quite expensive!
|
||||
// Don't worry, it's not exposed to any production-facing routes.
|
||||
const QUERY_CLIENT_DELETE = 'DELETE clients, codes, tokens, refreshTokens, clientDevelopers ' +
|
||||
'FROM clients ' +
|
||||
'LEFT JOIN codes ON clients.id = codes.clientId ' +
|
||||
'LEFT JOIN tokens ON clients.id = tokens.clientId ' +
|
||||
'LEFT JOIN refreshTokens ON clients.id = refreshTokens.clientId ' +
|
||||
'LEFT JOIN clientDevelopers ON clients.id = clientDevelopers.clientId ' +
|
||||
'WHERE clients.id=?';
|
||||
const QUERY_CODE_INSERT =
|
||||
'INSERT INTO codes (clientId, userId, email, scope, authAt, amr, aal, offline, code, codeChallengeMethod, codeChallenge, keysJwe, profileChangedAt) ' +
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
const QUERY_ACCESS_TOKEN_INSERT =
|
||||
'INSERT INTO tokens (clientId, userId, email, scope, type, expiresAt, ' +
|
||||
'token, profileChangedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
const QUERY_REFRESH_TOKEN_INSERT =
|
||||
'INSERT INTO refreshTokens (clientId, userId, email, scope, token, profileChangedAt) VALUES ' +
|
||||
'(?, ?, ?, ?, ?, ?)';
|
||||
const QUERY_ACCESS_TOKEN_FIND = 'SELECT * FROM tokens WHERE token=?';
|
||||
const QUERY_REFRESH_TOKEN_FIND = 'SELECT * FROM refreshTokens where token=?';
|
||||
const QUERY_REFRESH_TOKEN_LAST_USED_UPDATE = 'UPDATE refreshTokens SET lastUsedAt=? WHERE token=?';
|
||||
const QUERY_CODE_FIND = 'SELECT * FROM codes WHERE code=?';
|
||||
const QUERY_CODE_DELETE = 'DELETE FROM codes WHERE code=?';
|
||||
const QUERY_ACCESS_TOKEN_DELETE = 'DELETE FROM tokens WHERE token=?';
|
||||
const QUERY_REFRESH_TOKEN_DELETE = 'DELETE FROM refreshTokens WHERE token=?';
|
||||
const QUERY_ACCESS_TOKEN_DELETE_USER = 'DELETE FROM tokens WHERE userId=?';
|
||||
|
||||
const QUERY_DELETE_ACCESS_TOKEN_FOR_PUBLIC_CLIENTS = 'DELETE FROM tokens WHERE userId=? AND clientId IN (?);';
|
||||
const QUERY_DELETE_REFRESH_TOKEN_FOR_PUBLIC_CLIENTS = 'DELETE FROM refreshTokens WHERE userId=? AND clientId IN (?);';
|
||||
const QUERY_REFRESH_TOKEN_DELETE_USER =
|
||||
'DELETE FROM refreshTokens WHERE userId=?';
|
||||
const QUERY_CODE_DELETE_USER = 'DELETE FROM codes WHERE userId=?';
|
||||
const QUERY_DEVELOPER = 'SELECT * FROM developers WHERE email=?';
|
||||
const QUERY_DEVELOPER_DELETE = 'DELETE developers, clientDevelopers ' +
|
||||
'FROM developers ' +
|
||||
'LEFT JOIN clientDevelopers ON developers.developerId = clientDevelopers.developerId ' +
|
||||
'WHERE developers.email=?';
|
||||
const QUERY_PURGE_EXPIRED_TOKENS = 'DELETE FROM tokens WHERE clientId NOT IN (?) AND expiresAt < NOW() LIMIT ?;';
|
||||
const QUERY_EXPIRED_TOKENS =
|
||||
'SELECT expiresAt, token, clientId FROM tokens WHERE expiresAt >= ? AND expiresAt <= NOW() ORDER BY expiresAt ASC LIMIT ?';
|
||||
const QUERY_DELETE_EXPIRED_TOKENS = 'DELETE FROM tokens WHERE token IN (?) AND clientId NOT IN (?) AND expiresAt <= NOW()';
|
||||
const QUERY_LAST_PURGE_TIME = 'SELECT value FROM dbMetadata WHERE name = "last-purge-time"';
|
||||
const QUERY_REPLACE_LAST_PURGE_TIME = 'REPLACE INTO dbMetadata (name, value) VALUES ("last-purge-time", ?)';
|
||||
// Token management by uid.
|
||||
// Returns the most recent token used with a client name and client id.
|
||||
// Does not include clients that canGrant.
|
||||
const QUERY_ACTIVE_CLIENT_TOKENS_BY_UID =
|
||||
'SELECT tokens.clientId AS id, tokens.createdAt, tokens.scope, clients.name ' +
|
||||
'FROM tokens LEFT OUTER JOIN clients ON clients.id = tokens.clientId ' +
|
||||
'WHERE tokens.userId=? AND tokens.expiresAt > NOW() AND clients.canGrant = 0;';
|
||||
const DELETE_ACTIVE_CODES_BY_CLIENT_AND_UID =
|
||||
'DELETE FROM codes WHERE clientId=? AND userId=?';
|
||||
const DELETE_ACTIVE_TOKENS_BY_CLIENT_AND_UID =
|
||||
'DELETE FROM tokens WHERE clientId=? AND userId=?';
|
||||
const DELETE_ACTIVE_REFRESH_TOKENS_BY_CLIENT_AND_UID =
|
||||
'DELETE FROM refreshTokens WHERE clientId=? AND userId=?';
|
||||
// Scope queries
|
||||
const QUERY_SCOPE_FIND =
|
||||
'SELECT * ' +
|
||||
'FROM scopes ' +
|
||||
'WHERE scopes.scope=?;';
|
||||
const QUERY_SCOPES_INSERT =
|
||||
'INSERT INTO scopes (scope, hasScopedKeys) ' +
|
||||
'VALUES (?, ?);';
|
||||
|
||||
function firstRow(rows) {
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
function releaseConn(connection) {
|
||||
connection.release();
|
||||
}
|
||||
|
||||
MysqlStore.prototype = {
|
||||
|
||||
ping: function ping() {
|
||||
logger.debug('ping');
|
||||
// see bluebird.using():
|
||||
// https://github.com/petkaantonov/bluebird/blob/master/API.md#resource-management
|
||||
return P.using(this._getConnection(), function(conn) {
|
||||
return new P(function(resolve, reject) {
|
||||
conn.ping(function(err) {
|
||||
if (err) {
|
||||
logger.error('ping:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// createdAt is DEFAULT NOW() in the schema.sql
|
||||
registerClient: function registerClient(client) {
|
||||
var id;
|
||||
if (client.id) {
|
||||
id = buf(client.id);
|
||||
} else {
|
||||
id = unique.id();
|
||||
}
|
||||
logger.debug('registerClient', { name: client.name, id: hex(id) });
|
||||
return this._write(QUERY_CLIENT_REGISTER, [
|
||||
id,
|
||||
client.name,
|
||||
client.imageUri || '',
|
||||
buf(client.hashedSecret),
|
||||
client.hashedSecretPrevious ? buf(client.hashedSecretPrevious) : null,
|
||||
client.redirectUri,
|
||||
!! client.trusted,
|
||||
client.allowedScopes ? client.allowedScopes : null,
|
||||
!! client.canGrant,
|
||||
!! client.publicClient
|
||||
]).then(function() {
|
||||
logger.debug('registerClient.success', { id: hex(id) });
|
||||
client.id = id;
|
||||
return client;
|
||||
});
|
||||
},
|
||||
registerClientDeveloper: function regClientDeveloper(developerId, clientId) {
|
||||
if (! developerId || ! clientId) {
|
||||
var err = new Error('Owner registration requires user and developer id');
|
||||
return P.reject(err);
|
||||
}
|
||||
|
||||
var rowId = unique.id();
|
||||
|
||||
logger.debug('registerClientDeveloper', {
|
||||
rowId: rowId,
|
||||
developerId: developerId,
|
||||
clientId: clientId
|
||||
});
|
||||
|
||||
return this._write(QUERY_CLIENT_DEVELOPER_INSERT, [
|
||||
buf(rowId),
|
||||
buf(developerId),
|
||||
buf(clientId)
|
||||
]);
|
||||
},
|
||||
getClientDevelopers: function getClientDevelopers (clientId) {
|
||||
if (! clientId) {
|
||||
return P.reject(new Error('Client id is required'));
|
||||
}
|
||||
return this._read(QUERY_CLIENT_DEVELOPER_LIST_BY_CLIENT_ID, [
|
||||
buf(clientId)
|
||||
]);
|
||||
},
|
||||
activateDeveloper: function activateDeveloper(email) {
|
||||
if (! email) {
|
||||
return P.reject(new Error('Email is required'));
|
||||
}
|
||||
|
||||
var developerId = unique.developerId();
|
||||
logger.debug('activateDeveloper', { developerId: developerId });
|
||||
return this._write(QUERY_DEVELOPER_INSERT, [
|
||||
developerId, email
|
||||
]).then(function () {
|
||||
return this.getDeveloper(email);
|
||||
}.bind(this));
|
||||
},
|
||||
getDeveloper: function(email) {
|
||||
if (! email) {
|
||||
return P.reject(new Error('Email is required'));
|
||||
}
|
||||
|
||||
return this._readOne(QUERY_DEVELOPER, [
|
||||
email
|
||||
]);
|
||||
},
|
||||
removeDeveloper: function(email) {
|
||||
if (! email) {
|
||||
return P.reject(new Error('Email is required'));
|
||||
}
|
||||
|
||||
return this._write(QUERY_DEVELOPER_DELETE, [
|
||||
email
|
||||
]);
|
||||
},
|
||||
developerOwnsClient: function devOwnsClient(developerEmail, clientId) {
|
||||
return this._readOne(QUERY_DEVELOPER_OWNS_CLIENT, [
|
||||
developerEmail, buf(clientId)
|
||||
]).then(function(result) {
|
||||
if (result) {
|
||||
return P.resolve(true);
|
||||
} else {
|
||||
return P.reject(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateClient: function updateClient(client) {
|
||||
if (! client.id) {
|
||||
return P.reject(new Error('Update client needs an id'));
|
||||
}
|
||||
var secret = client.hashedSecret;
|
||||
if (secret) {
|
||||
secret = buf(secret);
|
||||
}
|
||||
|
||||
var secretPrevious = client.hashedSecretPrevious;
|
||||
if (secretPrevious) {
|
||||
secretPrevious = buf(secretPrevious);
|
||||
}
|
||||
return this._write(QUERY_CLIENT_UPDATE, [
|
||||
// VALUES
|
||||
client.name,
|
||||
client.imageUri,
|
||||
secret,
|
||||
secretPrevious,
|
||||
client.redirectUri,
|
||||
client.trusted,
|
||||
client.allowedScopes,
|
||||
client.canGrant,
|
||||
|
||||
// WHERE
|
||||
buf(client.id)
|
||||
]);
|
||||
},
|
||||
|
||||
getClient: function getClient(id) {
|
||||
return this._readOne(QUERY_CLIENT_GET, [buf(id)]);
|
||||
},
|
||||
getClients: function getClients(email) {
|
||||
return this._read(QUERY_CLIENT_LIST, [ email ]);
|
||||
},
|
||||
removeClient: function removeClient(id) {
|
||||
return this._write(QUERY_CLIENT_DELETE, [buf(id)]);
|
||||
},
|
||||
generateCode: function generateCode(codeObj) {
|
||||
var code = unique.code();
|
||||
var hash = encrypt.hash(code);
|
||||
return this._write(QUERY_CODE_INSERT, [
|
||||
codeObj.clientId,
|
||||
codeObj.userId,
|
||||
codeObj.email,
|
||||
codeObj.scope.toString(),
|
||||
codeObj.authAt,
|
||||
codeObj.amr ? codeObj.amr.join(',') : null,
|
||||
codeObj.aal || null,
|
||||
!! codeObj.offline,
|
||||
hash,
|
||||
codeObj.codeChallengeMethod,
|
||||
codeObj.codeChallenge,
|
||||
codeObj.keysJwe,
|
||||
codeObj.profileChangedAt
|
||||
]).then(function() {
|
||||
return code;
|
||||
});
|
||||
},
|
||||
getCode: function getCode(code) {
|
||||
var hash = encrypt.hash(code);
|
||||
return this._readOne(QUERY_CODE_FIND, [hash]).then(function(code) {
|
||||
if (code) {
|
||||
code.scope = ScopeSet.fromString(code.scope);
|
||||
if (code.amr !== null) {
|
||||
code.amr = code.amr.split(',');
|
||||
}
|
||||
}
|
||||
return code;
|
||||
});
|
||||
},
|
||||
removeCode: function removeCode(code) {
|
||||
var hash = encrypt.hash(code);
|
||||
return this._write(QUERY_CODE_DELETE, [hash]);
|
||||
},
|
||||
generateAccessToken: function generateAccessToken(vals) {
|
||||
var t = {
|
||||
clientId: buf(vals.clientId),
|
||||
userId: buf(vals.userId),
|
||||
email: vals.email,
|
||||
scope: vals.scope,
|
||||
token: unique.token(),
|
||||
type: 'bearer',
|
||||
expiresAt: vals.expiresAt || new Date(Date.now() + (vals.ttl * 1000 || MAX_TTL)),
|
||||
profileChangedAt: vals.profileChangedAt || 0
|
||||
};
|
||||
return this._write(QUERY_ACCESS_TOKEN_INSERT, [
|
||||
t.clientId,
|
||||
t.userId,
|
||||
t.email,
|
||||
t.scope.toString(),
|
||||
t.type,
|
||||
t.expiresAt,
|
||||
encrypt.hash(t.token),
|
||||
t.profileChangedAt,
|
||||
]).then(function() {
|
||||
return t;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an access token by token id
|
||||
* @param id Token Id
|
||||
* @returns {*}
|
||||
*/
|
||||
getAccessToken: function getAccessToken(id) {
|
||||
return this._readOne(QUERY_ACCESS_TOKEN_FIND, [buf(id)]).then(function(t) {
|
||||
if (t) {
|
||||
t.scope = ScopeSet.fromString(t.scope);
|
||||
}
|
||||
return t;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove token by token id
|
||||
* @param id
|
||||
* @returns {*}
|
||||
*/
|
||||
removeAccessToken: function removeAccessToken(id) {
|
||||
return this._write(QUERY_ACCESS_TOKEN_DELETE, [buf(id)]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all services that have have non-expired tokens
|
||||
* @param {String} uid User ID as hex
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getActiveClientsByUid: function getActiveClientsByUid(uid) {
|
||||
return this._read(QUERY_ACTIVE_CLIENT_TOKENS_BY_UID, [
|
||||
buf(uid)
|
||||
]).then(function(activeClientTokens) {
|
||||
activeClientTokens.forEach(t => {
|
||||
t.scope = ScopeSet.fromString(t.scope);
|
||||
});
|
||||
return helpers.aggregateActiveClients(activeClientTokens);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete all authorization grants for some clientId and uid.
|
||||
*
|
||||
* @param {String} clientId Client ID
|
||||
* @param {String} uid User Id as Hex
|
||||
* @returns {Promise}
|
||||
*/
|
||||
deleteClientAuthorization: function deleteClientAuthorization(clientId, uid) {
|
||||
const deleteCodes = this._write(DELETE_ACTIVE_CODES_BY_CLIENT_AND_UID, [
|
||||
buf(clientId),
|
||||
buf(uid)
|
||||
]);
|
||||
|
||||
const deleteTokens = this._write(DELETE_ACTIVE_TOKENS_BY_CLIENT_AND_UID, [
|
||||
buf(clientId),
|
||||
buf(uid)
|
||||
]);
|
||||
|
||||
const deleteRefreshTokens = this._write(DELETE_ACTIVE_REFRESH_TOKENS_BY_CLIENT_AND_UID, [
|
||||
buf(clientId),
|
||||
buf(uid)
|
||||
]);
|
||||
|
||||
return P.all([
|
||||
deleteCodes,
|
||||
deleteTokens,
|
||||
deleteRefreshTokens
|
||||
]);
|
||||
},
|
||||
|
||||
generateRefreshToken: function generateRefreshToken(vals) {
|
||||
var t = {
|
||||
clientId: vals.clientId,
|
||||
userId: vals.userId,
|
||||
email: vals.email,
|
||||
scope: vals.scope,
|
||||
profileChangedAt: vals.profileChangedAt
|
||||
};
|
||||
var token = unique.token();
|
||||
var hash = encrypt.hash(token);
|
||||
return this._write(QUERY_REFRESH_TOKEN_INSERT, [
|
||||
t.clientId,
|
||||
t.userId,
|
||||
t.email,
|
||||
t.scope.toString(),
|
||||
hash,
|
||||
t.profileChangedAt
|
||||
]).then(function() {
|
||||
t.token = token;
|
||||
return t;
|
||||
});
|
||||
},
|
||||
|
||||
getRefreshToken: function getRefreshToken(token) {
|
||||
return this._readOne(QUERY_REFRESH_TOKEN_FIND, [buf(token)])
|
||||
.then(function(t) {
|
||||
if (t) {
|
||||
t.scope = ScopeSet.fromString(t.scope);
|
||||
}
|
||||
return t;
|
||||
});
|
||||
},
|
||||
|
||||
usedRefreshToken: function usedRefreshToken(token) {
|
||||
var now = new Date();
|
||||
return this._write(QUERY_REFRESH_TOKEN_LAST_USED_UPDATE, [
|
||||
now,
|
||||
// WHERE
|
||||
token
|
||||
]);
|
||||
},
|
||||
|
||||
removeRefreshToken: function removeRefreshToken(id) {
|
||||
return this._write(QUERY_REFRESH_TOKEN_DELETE, [buf(id)]);
|
||||
},
|
||||
|
||||
getEncodingInfo: function getEncodingInfo() {
|
||||
var info = {};
|
||||
|
||||
var self = this;
|
||||
var qry = 'SHOW VARIABLES LIKE "%character\\_set\\_%"';
|
||||
return this._read(qry).then(function(rows) {
|
||||
rows.forEach(function(row) {
|
||||
info[row.Variable_name] = row.Value;
|
||||
});
|
||||
|
||||
qry = 'SHOW VARIABLES LIKE "%collation\\_%"';
|
||||
return self._read(qry).then(function(rows) {
|
||||
rows.forEach(function(row) {
|
||||
info[row.Variable_name] = row.Value;
|
||||
});
|
||||
return info;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
purgeExpiredTokens: function purgeExpiredTokens(numberOfTokens,
|
||||
delaySeconds,
|
||||
ignoreClientId,
|
||||
deleteBatchSize = 200)
|
||||
{
|
||||
const self = this;
|
||||
if (! ignoreClientId) {
|
||||
throw new Error('empty ignoreClientId');
|
||||
}
|
||||
|
||||
if (! Array.isArray(ignoreClientId)) {
|
||||
ignoreClientId = [ ignoreClientId ];
|
||||
}
|
||||
|
||||
const clientIds = ignoreClientId.map((id) => {
|
||||
return self.getClient(id);
|
||||
});
|
||||
|
||||
return P.all(clientIds)
|
||||
.then((results) => {
|
||||
// This ensures that purgeExpiredTokens can not be called with an
|
||||
// unknown ignoreClientId(s).
|
||||
results.forEach((ignoreClient) => {
|
||||
if (! ignoreClient) {
|
||||
throw new Error('unknown ignoreClientId ' + ignoreClientId);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
if (numberOfTokens <= deleteBatchSize) {
|
||||
deleteBatchSize = numberOfTokens;
|
||||
}
|
||||
|
||||
let deletedItems = 0;
|
||||
const promiseWhile = P.method(() => {
|
||||
if (deletedItems >= numberOfTokens) {
|
||||
const message = 'deletedItems >= numberOfTokens';
|
||||
logger.info('purgeExpiredTokens', {
|
||||
message: message,
|
||||
deletedItems: deletedItems,
|
||||
numberOfTokens: numberOfTokens,
|
||||
deleteBatchSize: deleteBatchSize
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const clientIn = ignoreClientId.map((id) => {
|
||||
return buf(id);
|
||||
});
|
||||
|
||||
return self._write(QUERY_PURGE_EXPIRED_TOKENS, [clientIn, deleteBatchSize])
|
||||
.then((res) => {
|
||||
logger.info('purgeExpiredTokens', { affectedRows: res.affectedRows });
|
||||
|
||||
// Break loop if no items were effected by delete.
|
||||
// All expired tokens have been deleted.
|
||||
if (res.affectedRows === 0) {
|
||||
const message = '0 affectedRows. Bailing out.';
|
||||
logger.info('purgeExpiredTokens', { message: message });
|
||||
return;
|
||||
}
|
||||
|
||||
deletedItems = deletedItems + res.affectedRows;
|
||||
|
||||
return P.delay(delaySeconds * 1000)
|
||||
.then(() => {
|
||||
return promiseWhile();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return promiseWhile();
|
||||
})
|
||||
.then(() => {
|
||||
logger.info('purgeExpiredTokens', { message: 'completed' });
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
// This version of purgeExpiredTokens uses the strategy of selecting a set
|
||||
// of tokens to delete and then issuing deletes by primary key.
|
||||
purgeExpiredTokensById: function purgeExpiredTokensById(numberOfTokens,
|
||||
delaySeconds,
|
||||
ignoreClientId,
|
||||
deleteBatchSize = 200)
|
||||
{
|
||||
const self = this;
|
||||
if (! ignoreClientId) {
|
||||
throw new Error('empty ignoreClientId');
|
||||
}
|
||||
|
||||
if (! Array.isArray(ignoreClientId)) {
|
||||
ignoreClientId = [ ignoreClientId ];
|
||||
}
|
||||
|
||||
if (numberOfTokens <= deleteBatchSize) {
|
||||
deleteBatchSize = numberOfTokens;
|
||||
}
|
||||
|
||||
const clientIds = ignoreClientId.map((id) => {
|
||||
return self.getClient(id);
|
||||
});
|
||||
|
||||
let lastPurgeTime;
|
||||
|
||||
return P.all(clientIds)
|
||||
.then((results) => {
|
||||
// This ensures that purgeExpiredTokensById can not be called with an
|
||||
// unknown ignoreClientId(s).
|
||||
results.forEach((ignoreClient) => {
|
||||
if (! ignoreClient) {
|
||||
throw new Error('unknown ignoreClientId ' + ignoreClientId);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Continue from the last recorded 'last-purge-time', if available.
|
||||
return self._readOne(QUERY_LAST_PURGE_TIME)
|
||||
.then((res) => {
|
||||
const OLDEST_POSSIBLE_TOKEN_EXPIRY = '2015-03-01 00:00:00';
|
||||
lastPurgeTime = (res && res.value) || OLDEST_POSSIBLE_TOKEN_EXPIRY;
|
||||
logger.info('purgeExpiredTokensById', { lastPurgeTime: lastPurgeTime });
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
let deletedItems = 0;
|
||||
const promiseWhile = P.method(() => {
|
||||
if (deletedItems >= numberOfTokens) {
|
||||
const message = 'deletedItems >= numberOfTokens';
|
||||
logger.info('purgeExpiredTokensById', {
|
||||
message: message,
|
||||
deletedItems: deletedItems,
|
||||
numberOfTokens: numberOfTokens,
|
||||
deleteBatchSize: deleteBatchSize
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const clientIn = ignoreClientId.map((id) => {
|
||||
return buf(id);
|
||||
});
|
||||
|
||||
return self._read(QUERY_EXPIRED_TOKENS, [lastPurgeTime, deleteBatchSize])
|
||||
.then((res) => {
|
||||
logger.info('purgeExpiredTokensById', { rowsReturned: res.length });
|
||||
|
||||
const tokensForDeletion = res.filter((row) => {
|
||||
const expiresAt = moment(row.expiresAt).format('YYYY-MM-DD HH:mm:ss');
|
||||
if (expiresAt > lastPurgeTime) {
|
||||
lastPurgeTime = expiresAt;
|
||||
}
|
||||
|
||||
if (ignoreClientId.includes(hex(row.clientId))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).map((row) => row.token);
|
||||
|
||||
// Break loop if we have no candidate rows to delete.
|
||||
if (tokensForDeletion.length === 0) {
|
||||
const message = '0 tokensForDeletion. Bailing out.';
|
||||
logger.info('purgeExpiredTokensById', { message: message });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('purgeExpiredTokensById', { tokensForDeletion: tokensForDeletion.length, lastPurgeTime: lastPurgeTime });
|
||||
|
||||
return self._write(QUERY_DELETE_EXPIRED_TOKENS, [ tokensForDeletion, clientIn ])
|
||||
.then((res) => {
|
||||
logger.info('purgeExpiredTokensById', { affectedRows: res.affectedRows });
|
||||
|
||||
// Break loop if no items were effected by delete.
|
||||
// All expired tokens have been deleted.
|
||||
if (res.affectedRows === 0) {
|
||||
const message = '0 affectedRows. Bailing out.';
|
||||
logger.info('purgeExpiredTokensById', { message: message });
|
||||
return;
|
||||
}
|
||||
|
||||
deletedItems = deletedItems + res.affectedRows;
|
||||
|
||||
logger.info('purgeExpiredTokensById', { lastPurgeTime: lastPurgeTime });
|
||||
// Update 'last-purge-time' and schedule next iteration.
|
||||
return self._write(QUERY_REPLACE_LAST_PURGE_TIME, [ lastPurgeTime ])
|
||||
.delay(delaySeconds * 1000)
|
||||
.then(() => {
|
||||
return promiseWhile();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return promiseWhile();
|
||||
})
|
||||
.then(() => {
|
||||
logger.info('purgeExpiredTokensById', { message: 'completed' });
|
||||
});
|
||||
},
|
||||
|
||||
removeUser: function removeUser(userId) {
|
||||
// TODO this should be a transaction or stored procedure
|
||||
var id = buf(userId);
|
||||
return this._write(QUERY_ACCESS_TOKEN_DELETE_USER, [id])
|
||||
.then(this._write.bind(this, QUERY_REFRESH_TOKEN_DELETE_USER, [id]))
|
||||
.then(this._write.bind(this, QUERY_CODE_DELETE_USER, [id]));
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes user's tokens and refreshTokens for canGrant and publicClient clients
|
||||
*
|
||||
* @param userId
|
||||
* @returns {Promise}
|
||||
*/
|
||||
removePublicAndCanGrantTokens: function removePublicAndCanGrantTokens(userId) {
|
||||
const uid = buf(userId);
|
||||
|
||||
return this._read(QUERY_PUBLIC_CLIENTS_LIST).then((_clients) => {
|
||||
const clientIds = _clients.map((client) => client.id);
|
||||
|
||||
return this._write(QUERY_DELETE_ACCESS_TOKEN_FOR_PUBLIC_CLIENTS, [uid, clientIds])
|
||||
.then(() => this._write(QUERY_DELETE_REFRESH_TOKEN_FOR_PUBLIC_CLIENTS, [uid, clientIds]));
|
||||
});
|
||||
},
|
||||
|
||||
getScope: function getScope (scope) {
|
||||
return this._readOne(QUERY_SCOPE_FIND, [scope]);
|
||||
},
|
||||
|
||||
registerScope: function registerScope (scope) {
|
||||
return this._write(QUERY_SCOPES_INSERT, [
|
||||
scope.scope,
|
||||
scope.hasScopedKeys
|
||||
]);
|
||||
},
|
||||
|
||||
_write: function _write(sql, params) {
|
||||
return this._query(sql, params);
|
||||
},
|
||||
|
||||
_read: function _read(sql, params) {
|
||||
return this._query(sql, params);
|
||||
},
|
||||
|
||||
_readOne: function _readOne(sql, params) {
|
||||
return this._read(sql, params).then(firstRow);
|
||||
},
|
||||
|
||||
_getConnection: function _getConnection() {
|
||||
// see bluebird.using()/disposer():
|
||||
// https://github.com/petkaantonov/bluebird/blob/master/API.md#resource-management
|
||||
//
|
||||
// tl;dr: using() and disposer() ensures that the dispose method will
|
||||
// ALWAYS be called at the end of the promise stack, regardless of
|
||||
// various errors thrown. So this should ALWAYS release the connection.
|
||||
var pool = this._pool;
|
||||
return new P(function(resolve, reject) {
|
||||
pool.getConnection(function(err, conn) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (conn._fxa_initialized) {
|
||||
return resolve(conn);
|
||||
}
|
||||
// Enforce sane defaults on every new connection.
|
||||
// These *should* be set by the database by default, but it's nice
|
||||
// to have an additional layer of protection here.
|
||||
conn.query('SELECT @@sql_mode AS mode', function(err, rows) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
var modes = rows[0]['mode'].split(',');
|
||||
var needToSetMode = false;
|
||||
REQUIRED_SQL_MODES.forEach(function(requiredMode) {
|
||||
if (modes.indexOf(requiredMode) === -1) {
|
||||
modes.push(requiredMode);
|
||||
needToSetMode = true;
|
||||
}
|
||||
});
|
||||
if (! needToSetMode) {
|
||||
conn._fxa_initialized = true;
|
||||
return resolve(conn);
|
||||
}
|
||||
var mode = modes.join(',');
|
||||
conn.query('SET SESSION sql_mode = \'' + mode + '\'', function(err) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
conn._fxa_initialized = true;
|
||||
return resolve(conn);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}).disposer(releaseConn);
|
||||
},
|
||||
|
||||
_query: function _query(sql, params) {
|
||||
return P.using(this._getConnection(), function(conn) {
|
||||
return new P(function(resolve, reject) {
|
||||
conn.query(sql, params || [], function(err, results) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = MysqlStore;
|
|
@ -1,9 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// The expected patch level of the database.
|
||||
// Update this if you add a new patch, and don't forget to update
|
||||
// the documentation for the current schema in ../schema.sql.
|
||||
|
||||
module.exports.level = 23;
|
|
@ -1,9 +0,0 @@
|
|||
-- Create the 'dbMetadata' table.
|
||||
-- Note: This should be the only thing in this initial patch.
|
||||
|
||||
CREATE TABLE dbMetadata (
|
||||
name VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
value VARCHAR(255) NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO dbMetadata SET name = 'schema-patch-level', value = '1';
|
|
@ -1,2 +0,0 @@
|
|||
-- -- drop the dbMetadata table
|
||||
-- DROP TABLE dbMetadata;
|
|
@ -1,45 +0,0 @@
|
|||
-- Create the initial set of tables.
|
||||
--
|
||||
-- Since this is the first migration, we use `IF NOT EXISTS` to allow us
|
||||
-- to run this on a db that already has the original schema in place. The
|
||||
-- patch will then be a no-op. Subsequent patches should *not* use `IF
|
||||
-- NOT EXISTS` but should fail noisily if the db is in an unexpected state.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id BINARY(8) PRIMARY KEY,
|
||||
secret BINARY(32) NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
imageUri VARCHAR(256) NOT NULL,
|
||||
redirectUri VARCHAR(256) NOT NULL,
|
||||
whitelisted BOOLEAN DEFAULT FALSE,
|
||||
canGrant BOOLEAN DEFAULT FALSE,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS codes (
|
||||
code BINARY(32) PRIMARY KEY,
|
||||
clientId BINARY(8) NOT NULL,
|
||||
INDEX codes_client_id(clientId),
|
||||
FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE,
|
||||
userId BINARY(16) NOT NULL,
|
||||
INDEX codes_user_id(userId),
|
||||
email VARCHAR(256) NOT NULL,
|
||||
scope VARCHAR(256) NOT NULL,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens (
|
||||
token BINARY(32) PRIMARY KEY,
|
||||
clientId BINARY(8) NOT NULL,
|
||||
INDEX tokens_client_id(clientId),
|
||||
FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE,
|
||||
userId BINARY(16) NOT NULL,
|
||||
INDEX tokens_user_id(userId),
|
||||
email VARCHAR(256) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL,
|
||||
scope VARCHAR(256) NOT NULL,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
UPDATE dbMetadata SET value = '2' WHERE name = 'schema-patch-level';
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
-- Drop all the tables.
|
||||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- DROP TABLE clients;
|
||||
-- DROP TABLE codes;
|
||||
-- DROP TABLE tokens;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '1' WHERE name = 'schema-patch-level';
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
-- Add `authAt` column to the `codes` table.
|
||||
|
||||
ALTER TABLE codes ADD COLUMN authAt BIGINT DEFAULT 0;
|
||||
|
||||
UPDATE dbMetadata SET value = '3' WHERE name = 'schema-patch-level';
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
-- Remove `authAt` column from the `codes` table.
|
||||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- ALTER TABLE codes DROP COLUMN authAt;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '2' WHERE name = 'schema-patch-level';
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
-- Adds support for Client Developers for OAuth clients
|
||||
|
||||
CREATE TABLE developers (
|
||||
developerId BINARY(16) NOT NULL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(email)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
CREATE TABLE clientDevelopers (
|
||||
rowId BINARY(8) NOT NULL PRIMARY KEY,
|
||||
developerId BINARY(16) NOT NULL,
|
||||
FOREIGN KEY (developerId) REFERENCES developers(developerId) ON DELETE CASCADE,
|
||||
clientId BINARY(8) NOT NULL,
|
||||
FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
UPDATE dbMetadata SET value = '4' WHERE name = 'schema-patch-level';
|
|
@ -1,6 +0,0 @@
|
|||
-- Remove support for Client Developers for OAuth clients
|
||||
|
||||
-- DROP TABLE clientDevelopers;
|
||||
-- DROP TABLE developers;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '3' WHERE name = 'schema-patch-level';
|
|
@ -1,13 +0,0 @@
|
|||
-- Begins process of renaming "whitelisted" to "trusted".
|
||||
-- We need to drop the "whitelisted" column in a separate patch in order
|
||||
-- to safely deploy this without downtime.
|
||||
|
||||
ALTER TABLE clients ADD COLUMN trusted BOOLEAN DEFAULT FALSE;
|
||||
UPDATE clients SET trusted=whitelisted;
|
||||
|
||||
-- Adds new "termsUri" and "privacyUri" columns for third-party clients.
|
||||
|
||||
ALTER TABLE clients ADD COLUMN termsUri VARCHAR(256) NOT NULL AFTER redirectUri;
|
||||
ALTER TABLE clients ADD COLUMN privacyUri VARCHAR(256) NOT NULL AFTER termsUri;
|
||||
|
||||
UPDATE dbMetadata SET value = '5' WHERE name = 'schema-patch-level';
|
|
@ -1,9 +0,0 @@
|
|||
-- Remove "termsUri" and "privacyUri" columns".
|
||||
-- Remove "trusted" column, ensuring to sync with old "whitelist" column.
|
||||
|
||||
-- ALTER TABLE clients DROP COLUMN privacyUri;
|
||||
-- ALTER TABLE clients DROP COLUMN termsUri;
|
||||
-- UPDATE clients SET whitelisted=trusted;
|
||||
-- ALTER TABLE clients DROP COLUMN trusted;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '4' WHERE name = 'schema-patch-level';
|
|
@ -1,24 +0,0 @@
|
|||
-- Add refreshTokens
|
||||
|
||||
CREATE TABLE refreshTokens (
|
||||
token BINARY(32) PRIMARY KEY,
|
||||
clientId BINARY(8) NOT NULL,
|
||||
INDEX tokens_client_id(clientId),
|
||||
FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE,
|
||||
userId BINARY(16) NOT NULL,
|
||||
INDEX tokens_user_id(userId),
|
||||
email VARCHAR(256) NOT NULL,
|
||||
scope VARCHAR(256) NOT NULL,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
-- Add expiresAt column for access tokens
|
||||
|
||||
ALTER TABLE tokens ADD COLUMN expiresAt TIMESTAMP NOT NULL DEFAULT "1980-01-01 00:00:00";
|
||||
UPDATE tokens SET expiresAt = DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 2 WEEK);
|
||||
|
||||
-- Add offline column to codes
|
||||
|
||||
ALTER TABLE codes ADD COLUMN offline BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
UPDATE dbMetadata SET value = '6' WHERE name = 'schema-patch-level';
|
|
@ -1,8 +0,0 @@
|
|||
-- Remove refreshToken table and expiresAt column from tokens table.
|
||||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- DROP TABLE refreshTokens;
|
||||
-- ALTER TABLE tokens DROP COLUMN expiresAt;
|
||||
-- ALTER TABLE codes DROP COLUMN offline;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '5' WHERE name = 'schema-patch-level';
|
|
@ -1,7 +0,0 @@
|
|||
-- Change clients.secret to clients.hashedSecret
|
||||
-- Drop whitelisted column
|
||||
|
||||
ALTER TABLE clients CHANGE secret hashedSecret BINARY(32);
|
||||
ALTER TABLE clients DROP COLUMN whitelisted;
|
||||
|
||||
UPDATE dbMetadata SET value = '7' WHERE name = 'schema-patch-level';
|
|
@ -1,8 +0,0 @@
|
|||
-- Change clients.hashedSecret to clients.secret
|
||||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- ALTER TABLE clients CHANGE hashedSecret secret BINARY(32);
|
||||
-- ALTER TABLE clients ADD COLUMN whitelisted BOOLEAN DEFAULT FALSE;
|
||||
-- UPDATE clients SET whitelisted=trusted;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '6' WHERE name = 'schema-patch-level';
|
|
@ -1,5 +0,0 @@
|
|||
ALTER TABLE clients CHANGE hashedSecret secret BINARY(32);
|
||||
ALTER TABLE clients ADD COLUMN whitelisted BOOLEAN DEFAULT FALSE;
|
||||
UPDATE clients SET whitelisted=trusted;
|
||||
|
||||
UPDATE dbMetadata SET value = '8' WHERE name = 'schema-patch-level';
|
|
@ -1,8 +0,0 @@
|
|||
-- Change clients.secret to clients.hashedSecret
|
||||
-- Drop whitelisted column
|
||||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- ALTER TABLE clients CHANGE secret hashedSecret BINARY(32);
|
||||
-- ALTER TABLE clients DROP COLUMN whitelisted;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '7' WHERE name = 'schema-patch-level';
|
|
@ -1,6 +0,0 @@
|
|||
-- Add hashedSecret column, to replace secret column.
|
||||
|
||||
ALTER TABLE clients ADD COLUMN hashedSecret BINARY(32);
|
||||
UPDATE clients SET hashedSecret = secret;
|
||||
|
||||
UPDATE dbMetadata SET value = '9' WHERE name = 'schema-patch-level';
|
|
@ -1,4 +0,0 @@
|
|||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- ALTER TABLE clients DROP COLUMN hashedSecret;
|
||||
-- UPDATE dbMetadata SET value = '8' WHERE name = 'schema-patch-level';
|
|
@ -1,5 +0,0 @@
|
|||
-- Remove `secret` column.
|
||||
|
||||
ALTER TABLE clients DROP COLUMN secret;
|
||||
|
||||
UPDATE dbMetadata SET value = '10' WHERE name = 'schema-patch-level';
|
|
@ -1,6 +0,0 @@
|
|||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- ALTER TABLE clients ADD COLUMN secret BINARY(32);
|
||||
-- UPDATE clients SET secret = hashedSecret;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '9' WHERE name = 'schema-patch-level';
|
|
@ -1,5 +0,0 @@
|
|||
-- dropping NOT NULL constraint
|
||||
|
||||
ALTER TABLE tokens MODIFY COLUMN email VARCHAR(256);
|
||||
|
||||
UPDATE dbMetadata SET value = '11' WHERE name = 'schema-patch-level';
|
|
@ -1,5 +0,0 @@
|
|||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- ALTER TABLE tokens MODIFY COLUMN email VARCHAR(256) NOT NULL;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '10' WHERE name = 'schema-patch-level';
|
|
@ -1,5 +0,0 @@
|
|||
-- Drop whitelisted column
|
||||
|
||||
ALTER TABLE clients DROP COLUMN whitelisted;
|
||||
|
||||
UPDATE dbMetadata SET value = '12' WHERE name = 'schema-patch-level';
|
|
@ -1,6 +0,0 @@
|
|||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- ALTER TABLE clients ADD COLUMN whitelisted BOOLEAN DEFAULT FALSE;
|
||||
-- UPDATE clients SET whitelisted=trusted;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '11' WHERE name = 'schema-patch-level';
|
|
@ -1,7 +0,0 @@
|
|||
-- Re-create NOT NULL constraint on the email column.
|
||||
-- We weren't able to drop it in production, this migration
|
||||
-- brings it back in our dev environments.
|
||||
|
||||
ALTER TABLE tokens MODIFY COLUMN email VARCHAR(256) NOT NULL;
|
||||
|
||||
UPDATE dbMetadata SET value = '13' WHERE name = 'schema-patch-level';
|
|
@ -1,5 +0,0 @@
|
|||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- ALTER TABLE tokens MODIFY COLUMN email VARCHAR(256);
|
||||
|
||||
-- UPDATE dbMetadata SET value = '12' WHERE name = 'schema-patch-level';
|
|
@ -1,5 +0,0 @@
|
|||
-- Add hashedSecretPrevious column
|
||||
|
||||
ALTER TABLE clients ADD COLUMN hashedSecretPrevious BINARY(32);
|
||||
|
||||
UPDATE dbMetadata SET value = '14' WHERE name = 'schema-patch-level';
|
|
@ -1,5 +0,0 @@
|
|||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- ALTER TABLE clients DROP COLUMN hashedSecretPrevious;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '13' WHERE name = 'schema-patch-level';
|
|
@ -1,5 +0,0 @@
|
|||
-- Add index idx_expiresAt to token table
|
||||
|
||||
ALTER TABLE tokens ADD INDEX idx_expiresAt (expiresAt), ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
UPDATE dbMetadata SET value = '15' WHERE name = 'schema-patch-level';
|
|
@ -1,5 +0,0 @@
|
|||
-- (commented out to avoid accidentally running this in production...)
|
||||
|
||||
-- ALTER TABLE tokens DROP INDEX idx_expiresAt;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '14' WHERE name = 'schema-patch-level';
|
|
@ -1,6 +0,0 @@
|
|||
-- Remove "termsUri" and "privacyUri" columns".
|
||||
|
||||
ALTER TABLE clients DROP COLUMN privacyUri;
|
||||
ALTER TABLE clients DROP COLUMN termsUri;
|
||||
|
||||
UPDATE dbMetadata SET value = '16' WHERE name = 'schema-patch-level';
|
|
@ -1,6 +0,0 @@
|
|||
-- Adds new "termsUri" and "privacyUri" columns for third-party clients.
|
||||
|
||||
--ALTER TABLE clients ADD COLUMN termsUri VARCHAR(256) NOT NULL AFTER redirectUri;
|
||||
--ALTER TABLE clients ADD COLUMN privacyUri VARCHAR(256) NOT NULL AFTER termsUri;
|
||||
|
||||
--UPDATE dbMetadata SET value = '15' WHERE name = 'schema-patch-level';
|
|
@ -1,6 +0,0 @@
|
|||
-- Add `lastUsedAt` column to the `refreshTokens` table.
|
||||
|
||||
ALTER TABLE refreshTokens ADD COLUMN lastUsedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
UPDATE dbMetadata SET value = '17' WHERE name = 'schema-patch-level';
|
|
@ -1,6 +0,0 @@
|
|||
-- Drop `lastUsedAt` column to the `refreshTokens` table.
|
||||
|
||||
-- ALTER TABLE refreshTokens DROP COLUMN lastUsedAt,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '16' WHERE name = 'schema-patch-level';
|
|
@ -1,11 +0,0 @@
|
|||
-- Add `publicClient` column to the `clients` table.
|
||||
ALTER TABLE clients ADD COLUMN publicClient BOOLEAN DEFAULT FALSE NOT NULL AFTER canGrant;
|
||||
UPDATE clients SET publicClient=false;
|
||||
|
||||
-- Add `codeChallengeMethod` and `codeChallenge` column to the `codes` table.
|
||||
ALTER TABLE codes
|
||||
ADD COLUMN codeChallengeMethod VARCHAR(256) AFTER offline,
|
||||
ADD COLUMN codeChallenge VARCHAR(256) AFTER codeChallengeMethod,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
UPDATE dbMetadata SET value = '18' WHERE name = 'schema-patch-level';
|
|
@ -1,14 +0,0 @@
|
|||
-- Drop `publicClient` column from the `clients` table.
|
||||
|
||||
-- ALTER TABLE clients DROP COLUMN publicClient,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- Drop `codeChallengeMethod` and `codeChallenge` column from the `codes` table.
|
||||
|
||||
-- ALTER TABLE codes DROP COLUMN codeChallengeMethod,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- ALTER TABLE codes DROP COLUMN codeChallenge,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '17' WHERE name = 'schema-patch-level';
|
|
@ -1,4 +0,0 @@
|
|||
ALTER TABLE codes ADD COLUMN keysJwe MEDIUMTEXT,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
UPDATE dbMetadata SET value = '19' WHERE name = 'schema-patch-level';
|
|
@ -1,4 +0,0 @@
|
|||
-- ALTER TABLE codes DROP COLUMN keysJwe,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '18' WHERE name = 'schema-patch-level';
|
|
@ -1,10 +0,0 @@
|
|||
ALTER TABLE clients
|
||||
ADD COLUMN allowedScopes VARCHAR(1024) AFTER trusted,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
CREATE TABLE scopes (
|
||||
scope VARCHAR(128) NOT NULL PRIMARY KEY,
|
||||
hasScopedKeys BOOLEAN NOT NULL DEFAULT FALSE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
UPDATE dbMetadata SET value = '20' WHERE name = 'schema-patch-level';
|
|
@ -1,6 +0,0 @@
|
|||
-- ALTER TABLE clients DROP COLUMN allowedScopes,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- DROP TABLE scopes;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '19' WHERE name = 'schema-patch-level';
|
|
@ -1,14 +0,0 @@
|
|||
-- Add columns to stash 'amr' and 'aal' on the codes table.
|
||||
-- The 'amr' column will be a comma-separated string.
|
||||
-- It's tempting to use MySQL's SET datatype to save on storage space here:
|
||||
--
|
||||
-- https://dev.mysql.com/doc/refman/5.7/en/set.html
|
||||
--
|
||||
-- But codes are transient, so it doesn't seem worthwhile to
|
||||
-- trade the maintenance complexity for a little storage space.
|
||||
ALTER TABLE codes
|
||||
ADD COLUMN amr VARCHAR(128) AFTER authAt,
|
||||
ADD COLUMN aal TINYINT AFTER amr,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
UPDATE dbMetadata SET value = '21' WHERE name = 'schema-patch-level';
|
|
@ -1,6 +0,0 @@
|
|||
-- ALTER TABLE codes
|
||||
-- DROP COLUMN amr,
|
||||
-- DROP COLUMN aal,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '20' WHERE name = 'schema-patch-level';
|
|
@ -1,11 +0,0 @@
|
|||
-- Add column to stash the `profileChangedAt` value
|
||||
ALTER TABLE codes ADD COLUMN profileChangedAt BIGINT DEFAULT NULL,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
ALTER TABLE tokens ADD COLUMN profileChangedAt BIGINT DEFAULT NULL,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
ALTER TABLE refreshTokens ADD COLUMN profileChangedAt BIGINT DEFAULT NULL,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
UPDATE dbMetadata SET value = '22' WHERE name = 'schema-patch-level';
|
|
@ -1,13 +0,0 @@
|
|||
-- ALTER TABLE tokens
|
||||
-- DROP COLUMN profileChangedAt,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- ALTER TABLE codes
|
||||
-- DROP COLUMN profileChangedAt,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- ALTER TABLE refreshTokens
|
||||
-- DROP COLUMN profileChangedAt,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '21`' WHERE name = 'schema-patch-level';
|
|
@ -1,55 +0,0 @@
|
|||
-- Drop foreign key constraints. They make DB migrations harder
|
||||
-- and aren't really providing us much value in practice.
|
||||
|
||||
-- The `clientDevelopers` table needs indexes on `developerId` a `clientId`
|
||||
-- for fast lookup. Prior to this patch, we were taking advantage of the
|
||||
-- index that is automatically created to enforce foreign key constraints,
|
||||
-- which the MySQL docs at [1] describe as:
|
||||
--
|
||||
-- """
|
||||
-- In the referencing table, there must be an index where the foreign key
|
||||
-- columns are listed as the first columns in the same order. Such an index
|
||||
-- is created on the referencing table automatically if it does not exist.
|
||||
-- This index might be silently dropped later, if you create another index
|
||||
-- that can be used to enforce the foreign key constraint.
|
||||
-- """
|
||||
-- [1] https://dev.mysql.com/doc/refman/5.7/en/create-table-foreign-keys.html
|
||||
--
|
||||
-- The "might" in there leaves some doubt about the exact circumstances under
|
||||
-- which we can depend on this index continuing to exist, so this migration
|
||||
-- explicitly creates the indexes we need. It's a two step process:
|
||||
--
|
||||
-- 1) Explicitly create the indexes we need. This "might" cause the ones
|
||||
-- that were created automatically for the FK constraint to be dropped.
|
||||
--
|
||||
-- 2) Drop the FK constraints, which might leave behind the auto-created
|
||||
-- indexes if they weren't dropped in (1) above.
|
||||
--
|
||||
-- In my testing, the auto-created indexes are indeed dropped in favour
|
||||
-- of the explicit ones. If they aren't, then at least we wind up with
|
||||
-- duplicate indexes which can be cleaned up manually, which is much better
|
||||
-- than winding up with no indexes at all.
|
||||
--
|
||||
|
||||
ALTER TABLE clientDevelopers ADD INDEX idx_clientDevelopers_developerId(developerId),
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
ALTER TABLE clientDevelopers ADD INDEX idx_clientDevelopers_clientId(clientId),
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
ALTER TABLE clientDevelopers DROP FOREIGN KEY clientDevelopers_ibfk_1,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
ALTER TABLE clientDevelopers DROP FOREIGN KEY clientDevelopers_ibfk_2,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
ALTER TABLE refreshTokens DROP FOREIGN KEY refreshTokens_ibfk_1,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
ALTER TABLE codes DROP FOREIGN KEY codes_ibfk_1,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
ALTER TABLE tokens DROP FOREIGN KEY tokens_ibfk_1,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
UPDATE dbMetadata SET value = '23' WHERE name = 'schema-patch-level';
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
-- ALTER TABLE clientDevelopers DROP INDEX idx_clientDevelopers_developerId,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- ALTER TABLE clientDevelopers DROP INDEX idx_clientDevelopers_clientId(clientId),
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- ALTER TABLE clientDevelopers ADD FOREIGN KEY (developerId) REFERENCES developers(developerId) ON DELETE CASCADE,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- ALTER TABLE clientDevelopers ADD FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- ALTER TABLE refreshTokens ADD FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- ALTER TABLE codes ADD FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- ALTER TABLE tokens ADD FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '22' WHERE name = 'schema-patch-level';
|
|
@ -1,84 +0,0 @@
|
|||
--
|
||||
-- This file represents the current db schema.
|
||||
-- It exists mainly for documentation purposes; any automated database
|
||||
-- modifications are controlled by the files in the ./patches/ directory.
|
||||
--
|
||||
-- If you make a change here, you should also create a new database patch
|
||||
-- file and increment the level in ./patch.js to reflect the change.
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id BINARY(8) PRIMARY KEY,
|
||||
hashedSecret BINARY(32),
|
||||
hashedSecretPrevious BINARY(32),
|
||||
name VARCHAR(256) NOT NULL,
|
||||
imageUri VARCHAR(256) NOT NULL,
|
||||
redirectUri VARCHAR(256) NOT NULL,
|
||||
canGrant BOOLEAN DEFAULT FALSE,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
trusted BOOLEAN DEFAULT FALSE,
|
||||
allowedScopes VARCHAR(1024)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS codes (
|
||||
code BINARY(32) PRIMARY KEY,
|
||||
clientId BINARY(8) NOT NULL,
|
||||
INDEX codes_client_id(clientId),
|
||||
userId BINARY(16) NOT NULL,
|
||||
INDEX codes_user_id(userId),
|
||||
email VARCHAR(256) NOT NULL,
|
||||
scope VARCHAR(256) NOT NULL,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
authAt BIGINT DEFAULT 0,
|
||||
offline BOOLEAN DEFAULT FALSE,
|
||||
profileChangedAt BIGINT DEFAULT NULL
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens (
|
||||
token BINARY(32) PRIMARY KEY,
|
||||
clientId BINARY(8) NOT NULL,
|
||||
INDEX tokens_client_id(clientId),
|
||||
userId BINARY(16) NOT NULL,
|
||||
INDEX tokens_user_id(userId),
|
||||
email VARCHAR(256) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL,
|
||||
scope VARCHAR(256) NOT NULL,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expiresAt TIMESTAMP NOT NULL,
|
||||
profileChangedAt BIGINT DEFAULT NULL,
|
||||
INDEX idx_expiresAt(expiresAt)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS developers (
|
||||
developerId BINARY(16) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(email)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clientDevelopers (
|
||||
rowId BINARY(8) PRIMARY KEY,
|
||||
developerId BINARY(16) NOT NULL,
|
||||
clientId BINARY(8) NOT NULL,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_clientDevelopers_developerId(developerId),
|
||||
INDEX idx_clientDevelopers_clientId(clientId)
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS refreshTokens (
|
||||
token BINARY(32) PRIMARY KEY,
|
||||
clientId BINARY(8) NOT NULL,
|
||||
INDEX tokens_client_id(clientId),
|
||||
userId BINARY(16) NOT NULL,
|
||||
INDEX tokens_user_id(userId),
|
||||
email VARCHAR(256) NOT NULL,
|
||||
scope VARCHAR(256) NOT NULL,
|
||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
lastUsedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
profileChangedAt BIGINT DEFAULT NULL
|
||||
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
||||
CREATE TABLE scopes (
|
||||
scope VARCHAR(128) NOT NULL PRIMARY KEY,
|
||||
hasScopedKeys BOOLEAN NOT NULL DEFAULT FALSE
|
||||
) ENGINE=InnoDB;
|
|
@ -1,15 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const buf = require('buf').hex;
|
||||
|
||||
const config = require('./config');
|
||||
|
||||
exports.hash = function hash(value) {
|
||||
var sha = crypto.createHash(config.get('encrypt.hashAlg'));
|
||||
sha.update(buf(value));
|
||||
return sha.digest();
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const config = require('./config');
|
||||
|
||||
exports.isProdLike = function isProdLike() {
|
||||
var env = config.get('env');
|
||||
return env === 'prod' || env === 'stage';
|
||||
};
|
|
@ -1,288 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const util = require('util');
|
||||
|
||||
const DEFAULTS = {
|
||||
code: 500,
|
||||
error: 'Internal Server Error',
|
||||
errno: 999,
|
||||
info: 'https://github.com/mozilla/fxa-oauth-server/blob/master/docs/api.md#errors',
|
||||
message: 'Unspecified error'
|
||||
};
|
||||
|
||||
function merge(target, other) {
|
||||
var keys = Object.keys(other || {});
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
target[keys[i]] = other[keys[i]];
|
||||
}
|
||||
}
|
||||
|
||||
function AppError(options, extra, headers) {
|
||||
this.message = options.message || DEFAULTS.message;
|
||||
this.isBoom = true;
|
||||
if (options.stack) {
|
||||
this.stack = options.stack;
|
||||
} else {
|
||||
Error.captureStackTrace(this, AppError);
|
||||
}
|
||||
this.errno = options.errno || DEFAULTS.errno;
|
||||
this.output = {
|
||||
statusCode: options.code || DEFAULTS.code,
|
||||
payload: {
|
||||
code: options.code || DEFAULTS.code,
|
||||
errno: this.errno,
|
||||
error: options.error || DEFAULTS.error,
|
||||
message: this.message,
|
||||
info: options.info || DEFAULTS.info
|
||||
},
|
||||
headers: headers || {}
|
||||
};
|
||||
merge(this.output.payload, extra);
|
||||
}
|
||||
util.inherits(AppError, Error);
|
||||
|
||||
AppError.prototype.toString = function () {
|
||||
return 'Error: ' + this.message;
|
||||
};
|
||||
|
||||
AppError.prototype.header = function (name, value) {
|
||||
this.output.headers[name] = value;
|
||||
};
|
||||
|
||||
AppError.translate = function translate(response) {
|
||||
if (response instanceof AppError) {
|
||||
return response;
|
||||
}
|
||||
|
||||
var error;
|
||||
var payload = response.output.payload;
|
||||
if (payload.validation) {
|
||||
error = AppError.invalidRequestParameter(payload.validation);
|
||||
} else if (payload.statusCode === 415) {
|
||||
error = AppError.invalidContentType();
|
||||
} else {
|
||||
error = new AppError({
|
||||
message: payload.message,
|
||||
code: payload.statusCode,
|
||||
error: payload.error,
|
||||
errno: payload.errno,
|
||||
stack: response.stack
|
||||
});
|
||||
}
|
||||
|
||||
return error;
|
||||
};
|
||||
|
||||
AppError.unknownClient = function unknownClient(clientId) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 101,
|
||||
message: 'Unknown client'
|
||||
}, {
|
||||
clientId: clientId
|
||||
});
|
||||
};
|
||||
|
||||
AppError.incorrectSecret = function incorrectSecret(clientId) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 102,
|
||||
message: 'Incorrect secret'
|
||||
}, {
|
||||
clientId: clientId
|
||||
});
|
||||
};
|
||||
|
||||
AppError.incorrectRedirect = function incorrectRedirect(uri) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 103,
|
||||
message: 'Incorrect redirect_uri'
|
||||
}, {
|
||||
redirectUri: uri
|
||||
});
|
||||
};
|
||||
|
||||
AppError.invalidAssertion = function invalidAssertion() {
|
||||
return new AppError({
|
||||
code: 401,
|
||||
error: 'Bad Request',
|
||||
errno: 104,
|
||||
message: 'Invalid assertion'
|
||||
});
|
||||
};
|
||||
|
||||
AppError.unknownCode = function unknownCode(code) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 105,
|
||||
message: 'Unknown code'
|
||||
}, {
|
||||
requestCode: code
|
||||
});
|
||||
};
|
||||
|
||||
AppError.mismatchCode = function mismatchCode(code, clientId) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 106,
|
||||
message: 'Incorrect code'
|
||||
}, {
|
||||
requestCode: code,
|
||||
client: clientId
|
||||
});
|
||||
};
|
||||
|
||||
AppError.expiredCode = function expiredCode(code, expiredAt) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 107,
|
||||
message: 'Expired code'
|
||||
}, {
|
||||
requestCode: code,
|
||||
expiredAt: expiredAt
|
||||
});
|
||||
};
|
||||
|
||||
AppError.invalidToken = function invalidToken() {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 108,
|
||||
message: 'Invalid token'
|
||||
});
|
||||
};
|
||||
|
||||
AppError.invalidRequestParameter = function invalidRequestParameter(val) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 109,
|
||||
message: 'Invalid request parameter'
|
||||
}, {
|
||||
validation: val
|
||||
});
|
||||
};
|
||||
|
||||
AppError.invalidResponseType = function invalidResponseType() {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 110,
|
||||
message: 'Invalid response_type'
|
||||
});
|
||||
};
|
||||
|
||||
AppError.unauthorized = function unauthorized(reason) {
|
||||
return new AppError({
|
||||
code: 401,
|
||||
error: 'Unauthorized',
|
||||
errno: 111,
|
||||
message: 'Unauthorized for route'
|
||||
}, {
|
||||
detail: reason
|
||||
});
|
||||
};
|
||||
|
||||
AppError.forbidden = function forbidden() {
|
||||
return new AppError({
|
||||
code: 403,
|
||||
error: 'Forbidden',
|
||||
errno: 112,
|
||||
message: 'Forbidden'
|
||||
});
|
||||
};
|
||||
|
||||
AppError.invalidContentType = function invalidContentType() {
|
||||
return new AppError({
|
||||
code: 415,
|
||||
error: 'Unsupported Media Type',
|
||||
errno: 113,
|
||||
message: 'Content-Type must be either application/json or ' +
|
||||
'application/x-www-form-urlencoded'
|
||||
});
|
||||
};
|
||||
|
||||
AppError.invalidScopes = function invalidScopes(scopes) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Invalid scopes',
|
||||
errno: 114,
|
||||
message: 'Requested scopes are not allowed'
|
||||
}, {
|
||||
invalidScopes: scopes
|
||||
});
|
||||
};
|
||||
|
||||
AppError.expiredToken = function expiredToken(expiredAt) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 115,
|
||||
message: 'Expired token'
|
||||
}, {
|
||||
expiredAt: expiredAt
|
||||
});
|
||||
};
|
||||
|
||||
AppError.notPublicClient = function unknownClient(clientId) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 116,
|
||||
message: 'Not a public client'
|
||||
}, {
|
||||
clientId: clientId
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
AppError.mismatchCodeChallenge = function mismatchCodeChallenge(pkceHashValue) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 117,
|
||||
message: 'Incorrect code_challenge'
|
||||
}, {
|
||||
requestCodeChallenge: pkceHashValue
|
||||
});
|
||||
};
|
||||
|
||||
AppError.missingPkceParameters = function missingPkceParameters() {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'PKCE parameters missing',
|
||||
errno: 118,
|
||||
message: 'Public clients require PKCE OAuth parameters'
|
||||
});
|
||||
};
|
||||
|
||||
AppError.staleAuthAt = function staleAuthAt(authAt) {
|
||||
return new AppError({
|
||||
code: 401,
|
||||
error: 'Bad Request',
|
||||
errno: 119,
|
||||
message: 'Stale authentication timestamp'
|
||||
}, {
|
||||
authAt: authAt
|
||||
});
|
||||
};
|
||||
|
||||
AppError.mismatchAcr = function mismatchAcr(foundValue) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 120,
|
||||
message: 'Mismatch acr value'
|
||||
}, {foundValue});
|
||||
};
|
||||
|
||||
module.exports = AppError;
|
|
@ -1,64 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const config = require('./config').getProperties();
|
||||
const db = require('./db');
|
||||
const P = require('./promise');
|
||||
const env = require('./env');
|
||||
const logger = require('./logging')('events');
|
||||
const Sink = require('fxa-notifier-aws').Sink;
|
||||
const HEX_STRING = require('./validators').HEX_STRING;
|
||||
|
||||
let fxaEvents;
|
||||
|
||||
if (! config.events.region || ! config.events.queueUrl) {
|
||||
fxaEvents = {
|
||||
start: function start() {
|
||||
if (env.isProdLike()) {
|
||||
throw new Error('config.events must be included in prod');
|
||||
} else {
|
||||
logger.warn('accountEvent.unconfigured');
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
fxaEvents = new Sink(config.events.region, config.events.queueUrl);
|
||||
|
||||
fxaEvents.on('data', (message) => {
|
||||
const messageEvent = message.event;
|
||||
const uid = message.uid;
|
||||
logger.verbose('data', message);
|
||||
logger.info(message.event, {uid: uid});
|
||||
|
||||
if (! HEX_STRING.test(uid)) {
|
||||
message.del();
|
||||
return logger.warn('badDelete', {userId: uid});
|
||||
}
|
||||
|
||||
return P.resolve()
|
||||
.then(() => {
|
||||
switch (messageEvent) {
|
||||
case 'delete':
|
||||
return db.removeUser(uid);
|
||||
case 'reset':
|
||||
case 'passwordChange':
|
||||
return db.removePublicAndCanGrantTokens(uid);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
})
|
||||
.done(() => {
|
||||
message.del();
|
||||
},
|
||||
(err) => {
|
||||
logger.error(message.event, err);
|
||||
});
|
||||
});
|
||||
|
||||
fxaEvents.on('error', (err) => logger.error('accountEvent', err));
|
||||
fxaEvents.start = fxaEvents.fetch;
|
||||
}
|
||||
|
||||
|
||||
module.exports = fxaEvents;
|
|
@ -1,15 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const config = require('../config').get('logging');
|
||||
|
||||
const mozlog = require('mozlog')(config);
|
||||
|
||||
var root = mozlog(config.app);
|
||||
if (root.isEnabledFor('debug')) {
|
||||
root.warn('\t*** CAREFUL! Louder logs (less than INFO)' +
|
||||
' may include SECRETS! ***');
|
||||
}
|
||||
|
||||
module.exports = mozlog;
|
|
@ -1,53 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const logger = require('./')('summary');
|
||||
|
||||
function parsePayload(payload) {
|
||||
var payloadKeys = ['INVALID_PAYLOAD_OBJECT'];
|
||||
try {
|
||||
// given payload object might not be a valid object
|
||||
// See issue #410
|
||||
payloadKeys = Object.keys(payload);
|
||||
} catch (e) {
|
||||
// failed to parse payload keys.
|
||||
}
|
||||
return payloadKeys;
|
||||
}
|
||||
|
||||
module.exports = function summary(request, response) {
|
||||
/*eslint complexity: [2, 11] */
|
||||
if (request.method === 'options') {
|
||||
return;
|
||||
}
|
||||
var payload = request.payload || {};
|
||||
var query = request.query || {};
|
||||
var params = request.params || {};
|
||||
|
||||
var auth = request.auth && request.auth.credentials && {
|
||||
user: request.auth.credentials.user,
|
||||
scope: request.auth.credentials.scope
|
||||
};
|
||||
|
||||
|
||||
var line = {
|
||||
code: response.isBoom ? response.output.statusCode : response.statusCode,
|
||||
errno: response.errno || 0,
|
||||
method: request.method,
|
||||
path: request.path,
|
||||
agent: request.headers['user-agent'],
|
||||
t: Date.now() - request.info.received,
|
||||
client_id: payload.client_id || query.client_id || params.client_id,
|
||||
auth: auth,
|
||||
payload: parsePayload(payload),
|
||||
remoteAddressChain: request.app.remoteAddressChain
|
||||
};
|
||||
|
||||
if (line.code >= 500) {
|
||||
line.stack = response.stack;
|
||||
logger.error('summary', line);
|
||||
} else {
|
||||
logger.info('summary', line);
|
||||
}
|
||||
};
|
|
@ -1,6 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = require('bluebird');
|
||||
module.exports.onPossiblyUnhandledRejection();
|
|
@ -1,321 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const buf = require('buf').hex;
|
||||
const hex = require('buf').to.hex;
|
||||
const Joi = require('joi');
|
||||
const URI = require('urijs');
|
||||
|
||||
const AppError = require('../error');
|
||||
const config = require('../config');
|
||||
const db = require('../db');
|
||||
const logger = require('../logging')('routes.authorization');
|
||||
const P = require('../promise');
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
const validators = require('../validators');
|
||||
const verify = require('../browserid');
|
||||
|
||||
const CODE = 'code';
|
||||
const TOKEN = 'token';
|
||||
|
||||
const ACCESS_TYPE_ONLINE = 'online';
|
||||
const ACCESS_TYPE_OFFLINE = 'offline';
|
||||
|
||||
const ACR_VALUE_AAL2 = 'AAL2';
|
||||
|
||||
const PKCE_SHA256_CHALLENGE_METHOD = 'S256'; // This server only supports S256 PKCE, no 'plain'
|
||||
const PKCE_CODE_CHALLENGE_LENGTH = 43;
|
||||
|
||||
const MAX_TTL_S = config.get('expiration.accessToken') / 1000;
|
||||
|
||||
const UNTRUSTED_CLIENT_ALLOWED_SCOPES = ScopeSet.fromArray([
|
||||
'openid',
|
||||
'profile:uid',
|
||||
'profile:email',
|
||||
'profile:display_name'
|
||||
]);
|
||||
|
||||
const allowHttpRedirects = config.get('allowHttpRedirects');
|
||||
|
||||
var ALLOWED_SCHEMES = [
|
||||
'https'
|
||||
];
|
||||
|
||||
if (allowHttpRedirects === true) {
|
||||
// http scheme used when developing OAuth clients
|
||||
ALLOWED_SCHEMES.push('http');
|
||||
}
|
||||
|
||||
function isLocalHost(url) {
|
||||
var host = new URI(url).hostname();
|
||||
return host === 'localhost' || host === '127.0.0.1';
|
||||
}
|
||||
|
||||
function generateCode(claims, client, scope, req) {
|
||||
return db.generateCode({
|
||||
clientId: client.id,
|
||||
userId: buf(claims.uid),
|
||||
email: claims['fxa-verifiedEmail'],
|
||||
scope: scope,
|
||||
authAt: claims['fxa-lastAuthAt'],
|
||||
amr: claims['fxa-amr'],
|
||||
aal: claims['fxa-aal'],
|
||||
offline: req.payload.access_type === ACCESS_TYPE_OFFLINE,
|
||||
codeChallengeMethod: req.payload.code_challenge_method,
|
||||
codeChallenge: req.payload.code_challenge,
|
||||
keysJwe: req.payload.keys_jwe,
|
||||
profileChangedAt: claims['fxa-profileChangedAt']
|
||||
}).then(function(code) {
|
||||
logger.debug('redirecting', { uri: req.payload.redirect_uri });
|
||||
|
||||
code = hex(code);
|
||||
const redirect = URI(req.payload.redirect_uri)
|
||||
.addQuery({ state: req.payload.state, code });
|
||||
|
||||
const out = {
|
||||
code,
|
||||
state: req.payload.state,
|
||||
redirect: String(redirect)
|
||||
};
|
||||
logger.info('generateCode', {
|
||||
request: {
|
||||
client_id: req.payload.client_id,
|
||||
redirect_uri: req.payload.redirect_uri,
|
||||
scope: req.payload.scope,
|
||||
state: req.payload.state,
|
||||
response_type: req.payload.response_type
|
||||
},
|
||||
response: out
|
||||
});
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
function generateGrant(claims, client, scope, req) {
|
||||
return db.generateAccessToken({
|
||||
clientId: client.id,
|
||||
userId: buf(claims.uid),
|
||||
email: claims['fxa-verifiedEmail'],
|
||||
scope: scope,
|
||||
ttl: req.payload.ttl,
|
||||
profileChangedAt: claims['fxa-profileChangedAt']
|
||||
}).then(function(token) {
|
||||
return {
|
||||
access_token: hex(token.token),
|
||||
token_type: 'bearer',
|
||||
expires_in: Math.floor((token.expiresAt - Date.now()) / 1000),
|
||||
scope: scope.toString(),
|
||||
auth_at: claims['fxa-lastAuthAt']
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Check that PKCE is provided if and only if appropriate.
|
||||
function checkPKCEParams(req, client) {
|
||||
if (req.payload.response_type === TOKEN) {
|
||||
// Direct token grant can't use PKCE.
|
||||
if (req.payload.code_challenge_method) {
|
||||
throw new AppError.invalidRequestParameter('code_challenge_method');
|
||||
}
|
||||
if (req.payload.code_challenge) {
|
||||
throw new AppError.invalidRequestParameter('code_challenge');
|
||||
}
|
||||
} else if (client.publicClient) {
|
||||
// Public clients *must* use PKCE.
|
||||
if (! req.payload.code_challenge_method || ! req.payload.code_challenge) {
|
||||
logger.info('client.missingPkceParameters');
|
||||
throw AppError.missingPkceParameters();
|
||||
}
|
||||
} else {
|
||||
// non-Public Clients can't use PKCE.
|
||||
if (req.payload.code_challenge_method || req.payload.code_challenge) {
|
||||
logger.info('client.notPublicClient');
|
||||
throw AppError.notPublicClient({ id: req.payload.client_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validate: {
|
||||
payload: {
|
||||
client_id: validators.clientId,
|
||||
assertion: validators.assertion
|
||||
.required(),
|
||||
redirect_uri: Joi.string()
|
||||
.max(256)
|
||||
// uri validation ref: https://github.com/hapijs/joi/blob/master/API.md#stringurioptions
|
||||
.uri({
|
||||
scheme: ALLOWED_SCHEMES
|
||||
}),
|
||||
scope: validators.scope,
|
||||
response_type: Joi.string()
|
||||
.valid(CODE, TOKEN)
|
||||
.default(CODE),
|
||||
state: Joi.string()
|
||||
.max(256)
|
||||
.when('response_type', {
|
||||
is: TOKEN,
|
||||
then: Joi.optional(),
|
||||
otherwise: Joi.required()
|
||||
}),
|
||||
ttl: Joi.number()
|
||||
.positive()
|
||||
.max(MAX_TTL_S)
|
||||
.default(MAX_TTL_S)
|
||||
.when('response_type', {
|
||||
is: TOKEN,
|
||||
then: Joi.optional(),
|
||||
otherwise: Joi.forbidden()
|
||||
}),
|
||||
access_type: Joi.string()
|
||||
.valid(ACCESS_TYPE_OFFLINE, ACCESS_TYPE_ONLINE)
|
||||
.default(ACCESS_TYPE_ONLINE)
|
||||
.optional(),
|
||||
code_challenge_method: Joi.string()
|
||||
.valid(PKCE_SHA256_CHALLENGE_METHOD)
|
||||
.when('response_type', {
|
||||
is: CODE,
|
||||
then: Joi.optional(),
|
||||
otherwise: Joi.forbidden()
|
||||
}),
|
||||
code_challenge: Joi.string()
|
||||
.length(PKCE_CODE_CHALLENGE_LENGTH)
|
||||
.when('response_type', {
|
||||
is: CODE,
|
||||
then: Joi.optional(),
|
||||
otherwise: Joi.forbidden()
|
||||
}),
|
||||
keys_jwe: validators.jwe
|
||||
.when('response_type', {
|
||||
is: CODE,
|
||||
then: Joi.optional(),
|
||||
otherwise: Joi.forbidden()
|
||||
}),
|
||||
acr_values: Joi.string().max(256).optional().allow(null)
|
||||
}
|
||||
},
|
||||
response: {
|
||||
schema: Joi.object().keys({
|
||||
redirect: Joi.string(),
|
||||
code: Joi.string(),
|
||||
state: Joi.string(),
|
||||
access_token: validators.token,
|
||||
token_type: Joi.string().valid('bearer'),
|
||||
scope: Joi.string().allow(''),
|
||||
auth_at: Joi.number(),
|
||||
expires_in: Joi.number()
|
||||
}).with('access_token', [
|
||||
'token_type',
|
||||
'scope',
|
||||
'auth_at',
|
||||
'expires_in'
|
||||
]).with('code', [
|
||||
'state',
|
||||
'redirect',
|
||||
]).without('code', [
|
||||
'access_token'
|
||||
])
|
||||
},
|
||||
handler: async function authorizationEndpoint(req) {
|
||||
/*eslint complexity: [2, 13] */
|
||||
logger.debug('response_type', req.payload.response_type);
|
||||
var start = Date.now();
|
||||
var wantsGrant = req.payload.response_type === TOKEN;
|
||||
var exitEarly = false;
|
||||
var scope = ScopeSet.fromString(req.payload.scope || '');
|
||||
return P.all([
|
||||
verify(req.payload.assertion).then(function(claims) {
|
||||
logger.info('time.browserid_verify', { ms: Date.now() - start });
|
||||
if (! claims) {
|
||||
exitEarly = true;
|
||||
throw AppError.invalidAssertion();
|
||||
}
|
||||
|
||||
// Check to see if the acr value requested by oauth matches what is expected
|
||||
const acrValues = req.payload.acr_values;
|
||||
if (acrValues) {
|
||||
const acrTokens = acrValues.split('\s+');
|
||||
if (acrTokens.includes(ACR_VALUE_AAL2) && ! (claims['fxa-aal'] >= 2)) {
|
||||
throw AppError.mismatchAcr(claims['fxa-aal']);
|
||||
}
|
||||
}
|
||||
|
||||
// Any request for a key-bearing scope should be using a verified token.
|
||||
// Double-check that here as a defense-in-depth measure.
|
||||
if (! claims['fxa-tokenVerified']) {
|
||||
return P.each(scope.getScopeValues(), scope => {
|
||||
// Don't bother hitting the DB if other checks have failed.
|
||||
if (exitEarly) {
|
||||
return;
|
||||
}
|
||||
// We know only URL-format scopes can have keys,
|
||||
// so avoid trips to the DB for common scopes like 'profile'.
|
||||
if (scope.startsWith('https://')) {
|
||||
return db.getScope(scope).then(s => {
|
||||
if (s && s.hasScopedKeys) {
|
||||
exitEarly = true;
|
||||
throw AppError.invalidAssertion();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
return claims;
|
||||
});
|
||||
}
|
||||
return claims;
|
||||
}),
|
||||
db.getClient(Buffer.from(req.payload.client_id, 'hex')).then(function(client) {
|
||||
logger.info('time.db_get_client', { ms: Date.now() - start });
|
||||
if (exitEarly) {
|
||||
// assertion was invalid, we can just stop here
|
||||
return;
|
||||
}
|
||||
if (! client) {
|
||||
logger.debug('notFound', { id: req.payload.client_id });
|
||||
throw AppError.unknownClient(req.payload.client_id);
|
||||
} else if (! client.trusted) {
|
||||
var invalidScopes = scope.difference(UNTRUSTED_CLIENT_ALLOWED_SCOPES);
|
||||
if (! invalidScopes.isEmpty()) {
|
||||
throw AppError.invalidScopes(invalidScopes.getScopeValues());
|
||||
}
|
||||
}
|
||||
|
||||
var uri = req.payload.redirect_uri || client.redirectUri;
|
||||
|
||||
if (uri !== client.redirectUri) {
|
||||
logger.debug('redirect.mismatch', {
|
||||
param: uri,
|
||||
registered: client.redirectUri
|
||||
});
|
||||
|
||||
if (config.get('localRedirects') && isLocalHost(uri)) {
|
||||
logger.debug('redirect.local', { uri: uri });
|
||||
} else {
|
||||
throw AppError.incorrectRedirect(uri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (wantsGrant && ! client.canGrant) {
|
||||
logger.warn('implicitGrant.notAllowed', {
|
||||
id: req.payload.client_id
|
||||
});
|
||||
throw AppError.invalidResponseType();
|
||||
}
|
||||
|
||||
req.payload.redirect_uri = uri;
|
||||
|
||||
checkPKCEParams(req, client);
|
||||
|
||||
return client;
|
||||
}).catch(err => {
|
||||
exitEarly = true;
|
||||
throw err;
|
||||
}),
|
||||
scope,
|
||||
req
|
||||
])
|
||||
.spread(wantsGrant ? generateGrant : generateCode);
|
||||
}
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const db = require('../../db');
|
||||
const SCOPE_CLIENT_WRITE = require('../../auth_bearer').SCOPE_CLIENT_WRITE;
|
||||
|
||||
module.exports = {
|
||||
auth: {
|
||||
strategy: 'authBearer',
|
||||
scope: SCOPE_CLIENT_WRITE.getImplicantValues()
|
||||
},
|
||||
handler: async function activeServices(req) {
|
||||
var clientId = req.params.client_id;
|
||||
return db.deleteClientAuthorization(clientId, req.auth.credentials.user)
|
||||
.then(function() {
|
||||
return {};
|
||||
});
|
||||
}
|
||||
};
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче