Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Work so far on the new auth package #3894

Merged
merged 4 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 102 additions & 2 deletions examples-next/basic/.keystone/schema-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,56 @@ export type UserWhereInput = {
readonly oneTimeThing_not_ends_with_i?: Scalars['String'] | null;
readonly oneTimeThing_in?: ReadonlyArray<Scalars['String'] | null> | null;
readonly oneTimeThing_not_in?: ReadonlyArray<Scalars['String'] | null> | null;
readonly passwordResetToken_is_set?: Scalars['Boolean'] | null;
readonly passwordResetIssuedAt?: Scalars['String'] | null;
readonly passwordResetIssuedAt_not?: Scalars['String'] | null;
readonly passwordResetIssuedAt_lt?: Scalars['String'] | null;
readonly passwordResetIssuedAt_lte?: Scalars['String'] | null;
readonly passwordResetIssuedAt_gt?: Scalars['String'] | null;
readonly passwordResetIssuedAt_gte?: Scalars['String'] | null;
readonly passwordResetIssuedAt_in?: ReadonlyArray<
Scalars['String'] | null
> | null;
readonly passwordResetIssuedAt_not_in?: ReadonlyArray<
Scalars['String'] | null
> | null;
readonly passwordResetRedeemedAt?: Scalars['String'] | null;
readonly passwordResetRedeemedAt_not?: Scalars['String'] | null;
readonly passwordResetRedeemedAt_lt?: Scalars['String'] | null;
readonly passwordResetRedeemedAt_lte?: Scalars['String'] | null;
readonly passwordResetRedeemedAt_gt?: Scalars['String'] | null;
readonly passwordResetRedeemedAt_gte?: Scalars['String'] | null;
readonly passwordResetRedeemedAt_in?: ReadonlyArray<
Scalars['String'] | null
> | null;
readonly passwordResetRedeemedAt_not_in?: ReadonlyArray<
Scalars['String'] | null
> | null;
readonly magicAuthToken_is_set?: Scalars['Boolean'] | null;
readonly magicAuthIssuedAt?: Scalars['String'] | null;
readonly magicAuthIssuedAt_not?: Scalars['String'] | null;
readonly magicAuthIssuedAt_lt?: Scalars['String'] | null;
readonly magicAuthIssuedAt_lte?: Scalars['String'] | null;
readonly magicAuthIssuedAt_gt?: Scalars['String'] | null;
readonly magicAuthIssuedAt_gte?: Scalars['String'] | null;
readonly magicAuthIssuedAt_in?: ReadonlyArray<
Scalars['String'] | null
> | null;
readonly magicAuthIssuedAt_not_in?: ReadonlyArray<
Scalars['String'] | null
> | null;
readonly magicAuthRedeemedAt?: Scalars['String'] | null;
readonly magicAuthRedeemedAt_not?: Scalars['String'] | null;
readonly magicAuthRedeemedAt_lt?: Scalars['String'] | null;
readonly magicAuthRedeemedAt_lte?: Scalars['String'] | null;
readonly magicAuthRedeemedAt_gt?: Scalars['String'] | null;
readonly magicAuthRedeemedAt_gte?: Scalars['String'] | null;
readonly magicAuthRedeemedAt_in?: ReadonlyArray<
Scalars['String'] | null
> | null;
readonly magicAuthRedeemedAt_not_in?: ReadonlyArray<
Scalars['String'] | null
> | null;
};

export type UserWhereUniqueInput = {
Expand All @@ -139,7 +189,15 @@ export type SortUsersBy =
| 'something_ASC'
| 'something_DESC'
| 'oneTimeThing_ASC'
| 'oneTimeThing_DESC';
| 'oneTimeThing_DESC'
| 'passwordResetIssuedAt_ASC'
| 'passwordResetIssuedAt_DESC'
| 'passwordResetRedeemedAt_ASC'
| 'passwordResetRedeemedAt_DESC'
| 'magicAuthIssuedAt_ASC'
| 'magicAuthIssuedAt_DESC'
| 'magicAuthRedeemedAt_ASC'
| 'magicAuthRedeemedAt_DESC';

export type UserUpdateInput = {
readonly name?: Scalars['String'] | null;
Expand All @@ -149,6 +207,12 @@ export type UserUpdateInput = {
readonly roles?: Scalars['String'] | null;
readonly posts?: PostRelateToManyInput | null;
readonly something?: Scalars['String'] | null;
readonly passwordResetToken?: Scalars['String'] | null;
readonly passwordResetIssuedAt?: Scalars['String'] | null;
readonly passwordResetRedeemedAt?: Scalars['String'] | null;
readonly magicAuthToken?: Scalars['String'] | null;
readonly magicAuthIssuedAt?: Scalars['String'] | null;
readonly magicAuthRedeemedAt?: Scalars['String'] | null;
};

export type UsersUpdateInput = {
Expand All @@ -165,6 +229,12 @@ export type UserCreateInput = {
readonly posts?: PostRelateToManyInput | null;
readonly something?: Scalars['String'] | null;
readonly oneTimeThing?: Scalars['String'] | null;
readonly passwordResetToken?: Scalars['String'] | null;
readonly passwordResetIssuedAt?: Scalars['String'] | null;
readonly passwordResetRedeemedAt?: Scalars['String'] | null;
readonly magicAuthToken?: Scalars['String'] | null;
readonly magicAuthIssuedAt?: Scalars['String'] | null;
readonly magicAuthRedeemedAt?: Scalars['String'] | null;
};

export type UsersCreateInput = {
Expand Down Expand Up @@ -289,6 +359,24 @@ export type _ListSchemaFieldsInput = {

export type CacheControlScope = 'PUBLIC' | 'PRIVATE';

export type AuthErrorCode =
| 'PASSWORD_AUTH_FAILURE'
| 'PASSWORD_AUTH_IDENTITY_NOT_FOUND'
| 'PASSWORD_AUTH_SECRET_NOT_SET'
| 'PASSWORD_AUTH_MULTIPLE_IDENTITY_MATCHES'
| 'PASSWORD_AUTH_SECRET_MISMATCH'
| 'AUTH_TOKEN_REQUEST_IDENTITY_NOT_FOUND'
| 'AUTH_TOKEN_REQUEST_MULTIPLE_IDENTITY_MATCHES'
| 'AUTH_TOKEN_REDEMPTION_FAILURE'
| 'AUTH_TOKEN_REDEMPTION_IDENTITY_NOT_FOUND'
| 'AUTH_TOKEN_REDEMPTION_MULTIPLE_IDENTITY_MATCHES'
| 'AUTH_TOKEN_REDEMPTION_TOKEN_NOT_SET'
| 'AUTH_TOKEN_REDEMPTION_TOKEN_MISMATCH'
| 'AUTH_TOKEN_REDEMPTION_TOKEN_EXPIRED'
| 'AUTH_TOKEN_REDEMPTION_TOKEN_REDEEMED'
| 'INTERNAL_ERROR'
| 'CUSTOM_ERROR';

export type CreateInitialUserInput = {
readonly name?: Scalars['String'] | null;
readonly email?: Scalars['String'] | null;
Expand All @@ -314,7 +402,13 @@ export type UserListTypeInfo = {
| 'roles'
| 'posts'
| 'something'
| 'oneTimeThing';
| 'oneTimeThing'
| 'passwordResetToken'
| 'passwordResetIssuedAt'
| 'passwordResetRedeemedAt'
| 'magicAuthToken'
| 'magicAuthIssuedAt'
| 'magicAuthRedeemedAt';
backing: {
readonly id: string | number;
readonly name?: string | null;
Expand All @@ -325,6 +419,12 @@ export type UserListTypeInfo = {
readonly posts?: string | null;
readonly something?: string | null;
readonly oneTimeThing?: string | null;
readonly passwordResetToken?: string | null;
readonly passwordResetIssuedAt?: Date | null;
readonly passwordResetRedeemedAt?: Date | null;
readonly magicAuthToken?: string | null;
readonly magicAuthIssuedAt?: Date | null;
readonly magicAuthRedeemedAt?: Date | null;
};
inputs: {
where: UserWhereInput;
Expand Down
10 changes: 10 additions & 0 deletions examples-next/basic/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ const auth = createAuth({
isAdmin: true,
},
},
passwordResetLink: {
sendToken: function (args): void {
console.log(`Password reset info:`, args);
},
},
magicAuthLink: {
sendToken: function (args): void {
console.log(`Magic auth info:`, args);
},
},
});

// const isAccessAllowed = ({ session }: { session: any }) => !!session?.item?.isAdmin;
Expand Down
4 changes: 2 additions & 2 deletions examples-next/basic/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export const lists = createSchema({
},
hooks: {
resolveInput: ({ resolvedData, originalInput }) => {
console.log({ resolvedData, originalInput });
console.log('list hooks: resolveInput', { resolvedData, originalInput });
return resolvedData;
},
beforeChange({ resolvedData, originalInput }) {
console.log({ resolvedData, originalInput });
console.log('list hooks: beforeChange', { resolvedData, originalInput });
},
},
access: {
Expand Down
79 changes: 79 additions & 0 deletions packages-next/auth/HOOKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Auth Hooks Spec'ing

See:

- The current [hooks API ref](https://www.keystonejs.com/api/hooks#authentication-hooks)
- The current [hooks guide](https://www.keystonejs.com/guides/hooks)
- The [PR that added auth hooks](https://github.com/keystonejs/keystone/pull/2039) (I thought there'd be more relevant discussion in here but there isn't really)

Currently:

```js
keystone.createAuthStrategy({
type: PasswordAuthStrategy,
list: 'User',
hooks: {
resolveAuthInput: async (...) => {...},
validateAuthInput: async (...) => {...},
beforeAuth: async (...) => {...},
afterAuth: async (...) => {...},

beforeUnauth: async (...) => {...},
afterUnauth: async (...) => {...},
},
});
```

## New Operations

We now have **more potential auth-related operations**:

- `authenticate` (existing)
- `unauthenticate` (existing)
- `createInitialItem`
- `sendPasswordResetLink`
- `redeemPasswordResetLink`
- `sendMagicAuthLink`
- `redeemMagicAuthLink`

(See [existing operations](https://www.keystonejs.com/guides/hooks#operation).)

## Opinions

- We don't need hooks for the `createInitialItem` operation, it's once off
- Or.. is this how we collect metrics from the demo projects?
- We should maintain the separation between "resolve" (can modify `resolvedData`) AND "validate" (can add validation errors) for auth hooks
- We should _reuse_ the existing `resolveAuthInput` and `validateAuthInput` functions for the new auth operations (as we do with update/create)

## Usage

So usage becomes something like...?

```js
keystone.createAuthStrategy({
type: PasswordAuthStrategy,
list: 'User',
hooks: {
resolveAuthInput: async (...) => {...},
validateAuthInput: async (...) => {...},

beforeAuth: async (...) => {...},
afterAuth: async (...) => {...},

beforeUnauth: async (...) => {...},
afterUnauth: async (...) => {...},

beforeSendPasswordResetLink: async (...) => {...},
afterSendPasswordResetLink: async (...) => {...},

beforeRedeemPasswordResetLink: async (...) => {...},
afterRedeemPasswordResetLink: async (...) => {...},

beforeSendMagicAuthLink: async (...) => {...},
afterSendMagicAuthLink: async (...) => {...},

beforeRedeemMagicAuthLink: async (...) => {...},
afterRedeemMagicAuthLink: async (...) => {...},
},
});
```
9 changes: 9 additions & 0 deletions packages-next/auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Identity Protection

TODO: Edit

If we're trying to maintain the privacy of accounts (hopefully, yes) make some effort to prevent timing attacks
Note, we're not attempting to protect the hashing comparisson itself from timing attacks, just _the existance of an item_
We can't assume the work factor so can't include a pre-generated hash to compare but generating a new hash will create a similar delay
Changes to the work factor, latency loading the item(s) and many other factors will still be detectable by a dedicated attacker
This is far from perfect (but better than nothing)
18 changes: 15 additions & 3 deletions packages-next/auth/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,21 @@
- [ ] Create UI for the signout page
- [ ] Only generate the signout page if the config is enabled
- [x] Add a signout button to the Admin UI when the config is enabled
- [ ] Implement forgotten password & magic links @molomby
- [ ] Define the list
- [ ] Add the mutations
- [.] Implement forgotten password & magic links @molomby
- [x] Define the list/fields
- [x] Add the mutations (auth, get reset token, get magic link)
- [x] Don't error on failure; create types/union type; `UserPasswordAuthSuccess { item token } UserPasswordAuthFailure { code message }`
- [x] Refactor the list and field validation into `validateConfig()`
- [x] Build out redemption mutations
- [x] `Auth` to return set of fields (to be added to the list); move fields def from example app
- [x] `withAuth()` to configure the list config directly
- [x] Add suffix to config; use for types, mutations, field names, etc.
- [ ] Add config for `validUserConditions` as an optional set of GraphQL filters; slightly refactor loading of item(s)
- [ ] Fix the `withAuth` destructuring around fields
- [ ] Hooks – See notes in HOOKS.md
- [ ] Review/revise the [existing hooks](https://www.keystonejs.com/api/hooks#authentication-hooks)
- [ ] Implement hooks for the auth, reset pass and magic link
- [ ] Support rate limiting use case
- [ ] Generate the UI if it is enabled
- [ ] Wire up the UI
- [ ] Implement init first user @mitchell
Expand Down
Loading