Skip to content

Commit

Permalink
Use PBKDF2 for password hashing (#29)
Browse files Browse the repository at this point in the history
* Use PBKDF2 for password hashing

* Bump version to 0.9.0
  • Loading branch information
KyleJune authored Nov 16, 2021
1 parent 7005c4c commit 1ea0ebd
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 60 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# OAuth2 Server

[![version](https://img.shields.io/badge/release-0.8.1-success)](https://deno.land/x/oauth2_server@0.8.1)
[![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/oauth2_server@0.8.1/authorization_server.ts)
[![version](https://img.shields.io/badge/release-0.9.0-success)](https://deno.land/x/oauth2_server@0.9.0)
[![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/oauth2_server@0.9.0/authorization_server.ts)
[![CI](https://github.com/udibo/oauth2_server/workflows/CI/badge.svg)](https://github.com/udibo/oauth2_server/actions?query=workflow%3ACI)
[![codecov](https://codecov.io/gh/udibo/oauth2_server/branch/main/graph/badge.svg?token=8Q7TSUFWUY)](https://codecov.io/gh/udibo/oauth2_server)
[![license](https://img.shields.io/github/license/udibo/oauth2_server)](https://github.com/udibo/oauth2_server/blob/master/LICENSE)
Expand Down Expand Up @@ -44,19 +44,19 @@ also acting as an authorization server.

```ts
// Import from Deno's third party module registry
import { ResourceServer } from "https://deno.land/x/oauth2_server@0.8.1/resource_server.ts";
import { ResourceServer } from "https://deno.land/x/oauth2_server@0.9.0/resource_server.ts";
// Import from GitHub
import { ResourceServer } from "https://raw.githubusercontent.com/udibo/oauth2_server/0.8.1/resource_server.ts";
import { ResourceServer } from "https://raw.githubusercontent.com/udibo/oauth2_server/0.9.0/resource_server.ts";
```

The AuthorizationServer is an extension of the ResourceServer, adding methods
used by the authorize and token endpoints.

```ts
// Import from Deno's third party module registry
import { AuthorizationServer } from "https://deno.land/x/oauth2_server@0.8.1/authorization_server.ts";
import { AuthorizationServer } from "https://deno.land/x/oauth2_server@0.9.0/authorization_server.ts";
// Import from GitHub
import { AuthorizationServer } from "https://raw.githubusercontent.com/udibo/oauth2_server/0.8.1/authorization_server.ts";
import { AuthorizationServer } from "https://raw.githubusercontent.com/udibo/oauth2_server/0.9.0/authorization_server.ts";
```

## Usage
Expand All @@ -66,7 +66,7 @@ An example of how to use this module can be found
but it should give you an idea of how to use this module.

See
[deno docs](https://doc.deno.land/https/deno.land/x/oauth2_server@0.8.1/authorization_server.ts)
[deno docs](https://doc.deno.land/https/deno.land/x/oauth2_server@0.9.0/authorization_server.ts)
for more information.

### Grants
Expand Down
10 changes: 5 additions & 5 deletions adapters/oak/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ also acting as an authorization server.

```ts
// Import from Deno's third party module registry
import { OakResourceServer } from "https://deno.land/x/oauth2_server@0.8.0/adapters/oak/resource_server.ts";
import { OakResourceServer } from "https://deno.land/x/oauth2_server@0.9.0/adapters/oak/resource_server.ts";
// Import from GitHub
import { OakResourceServer } from "https://raw.githubusercontent.com/udibo/oauth2_server/0.8.0/adapters/oak/resource_server.ts";
import { OakResourceServer } from "https://raw.githubusercontent.com/udibo/oauth2_server/0.9.0/adapters/oak/resource_server.ts";
```

The AuthorizationServer is an extension of the ResourceServer, adding methods
used by the authorize and token endpoints.

```ts
// Import from Deno's third party module registry
import { OakAuthorizationServer } from "https://deno.land/x/oauth2_server@0.8.0/adapters/oak/authorization_server.ts";
import { OakAuthorizationServer } from "https://deno.land/x/oauth2_server@0.9.0/adapters/oak/authorization_server.ts";
// Import from GitHub
import { OakAuthorizationServer } from "https://raw.githubusercontent.com/udibo/oauth2_server/0.8.0/adapters/oak/authorization_server.ts";
import { OakAuthorizationServer } from "https://raw.githubusercontent.com/udibo/oauth2_server/0.9.0/adapters/oak/authorization_server.ts";
```

## Usage
Expand All @@ -42,5 +42,5 @@ An example of how to use this adapter module can be found
but it should give you an idea of how to use this module.

See
[deno docs](https://doc.deno.land/https/deno.land/x/oauth2_server@0.8.0/adapters/oak/authorization_server.ts)
[deno docs](https://doc.deno.land/https/deno.land/x/oauth2_server@0.9.0/adapters/oak/authorization_server.ts)
for more information.
2 changes: 2 additions & 0 deletions authorization_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ export {
challengeMethods,
DefaultScope,
generateCodeVerifier,
generateSalt,
hashPassword,
InvalidClientError,
InvalidGrantError,
InvalidRequestError,
Expand Down
2 changes: 2 additions & 0 deletions authorization_server_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ test("verify exports", () => {
"camelCase",
"challengeMethods",
"generateCodeVerifier",
"generateSalt",
"hashPassword",
"loginRedirectFactory",
"parseBasicAuth",
"snakeCase",
Expand Down
2 changes: 2 additions & 0 deletions examples/oak-localstorage/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export {
challengeMethods,
ClientCredentialsGrant,
generateCodeVerifier,
generateSalt,
hashPassword,
loginRedirectFactory,
OAuth2Error,
RefreshTokenGrant,
Expand Down
14 changes: 4 additions & 10 deletions examples/oak-localstorage/services/user.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { AbstractUserService, encodeBase64 } from "../deps.ts";
import { AbstractUserService, generateSalt, hashPassword } from "../deps.ts";
import { User } from "../models/user.ts";

function generateSalt(): string {
const salt = new Uint8Array(16);
crypto.getRandomValues(salt);
return encodeBase64(salt);
}

interface UserInternal {
id: string;
username: string;
Expand All @@ -31,7 +25,7 @@ export class UserService extends AbstractUserService<User> {
if (email) next.email = email;
if (password) {
next.salt = generateSalt();
next.hash = await this.hashPassword(password, next.salt);
next.hash = await hashPassword(password, next.salt);
}

localStorage.setItem(`username:${username}`, id);
Expand All @@ -52,7 +46,7 @@ export class UserService extends AbstractUserService<User> {

if (password) {
next.salt = generateSalt();
next.hash = await this.hashPassword(password, next.salt);
next.hash = await hashPassword(password, next.salt);
} else if (password === null) {
delete next.salt;
delete next.hash;
Expand Down Expand Up @@ -97,7 +91,7 @@ export class UserService extends AbstractUserService<User> {
let user: User | undefined = undefined;
if (internal) {
const { hash, salt } = internal;
if (hash && salt && await this.hashPassword(password, salt) === hash) {
if (hash && salt && await hashPassword(password, salt) === hash) {
user = await this.toExternal(internal);
}
}
Expand Down
6 changes: 5 additions & 1 deletion resource_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,11 @@ export { AbstractClientService } from "./services/client.ts";
export type { ClientServiceInterface } from "./services/client.ts";

export type { User } from "./models/user.ts";
export { AbstractUserService } from "./services/user.ts";
export {
AbstractUserService,
generateSalt,
hashPassword,
} from "./services/user.ts";
export type { UserServiceInterface } from "./services/user.ts";

export { SCOPE, Scope, SCOPE_TOKEN } from "./models/scope.ts";
Expand Down
2 changes: 2 additions & 0 deletions resource_server_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ test("verify exports", () => {
"camelCase",
"challengeMethods",
"generateCodeVerifier",
"generateSalt",
"hashPassword",
"loginRedirectFactory",
"parseBasicAuth",
"snakeCase",
Expand Down
47 changes: 36 additions & 11 deletions services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { encodeHex } from "../deps.ts";
import { ServerError } from "../errors.ts";

export interface UserServiceInterface<User> {
/** Hashes a password with optional salt. */
hashPassword(password: string, salt?: string): Promise<string>;
/** Retrieves an authenticated user if the username/password combination is correct. */
getAuthenticated(
username: string,
Expand All @@ -13,15 +11,6 @@ export interface UserServiceInterface<User> {

export abstract class AbstractUserService<User>
implements UserServiceInterface<User> {
/** Hashes a password with optional salt. Default implementation uses SHA-256 algorithm. */
async hashPassword(password: string, salt?: string): Promise<string> {
const data = (new TextEncoder()).encode(
password + (salt ? `:${salt}` : ""),
);
const buffer = await crypto.subtle.digest("SHA-256", data);
return (new TextDecoder()).decode(encodeHex(new Uint8Array(buffer)));
}

/** Retrieves an authenticated user if the username/password combination is correct. Not implemented by default. */
async getAuthenticated(
_username: string,
Expand All @@ -32,3 +21,39 @@ export abstract class AbstractUserService<User>
);
}
}

const encoder = new TextEncoder();
const decoder = new TextDecoder();

/** Generates random salt. The length is the number of bytes. */
export function generateSalt(length = 16): string {
const salt = new Uint8Array(length);
crypto.getRandomValues(salt);
return decoder.decode(encodeHex(salt));
}

/** Hashes a password with salt using the PBKDF2 algorithm with 100k SHA-256 iterations. */
export async function hashPassword(
password: string,
salt: string,
): Promise<string> {
const keyMaterial = await crypto.subtle.importKey(
"raw",
encoder.encode(password),
"PBKDF2",
false,
["deriveBits", "deriveKey"],
);
const derivedBits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: encoder.encode(salt),
iterations: 100000,
hash: "SHA-256",
},
keyMaterial,
256,
);
const buffer = new Uint8Array(derivedBits, 0, 32);
return decoder.decode(encodeHex(buffer));
}
69 changes: 43 additions & 26 deletions services/user_test.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,21 @@
import {
assertEquals,
assertNotEquals,
assertRejects,
assertStrictEquals,
test,
TestSuite,
} from "../test_deps.ts";
import { ServerError } from "../errors.ts";
import { UserService } from "./test_services.ts";
import { generateSalt, hashPassword } from "./user.ts";

const userService = new UserService();

const userServiceTests: TestSuite<void> = new TestSuite({
name: "UserService",
});

test(userServiceTests, "hashPassword", async () => {
const result = userService.hashPassword("hunter1");
assertStrictEquals(Promise.resolve(result), result);
assertEquals(
await result,
"73660a4f7bbfb98b3e04cd38b257f69b017fbb52d5d864a59459cc9e40c92e6a",
);
assertEquals(
await userService.hashPassword("hunter2"),
"f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7",
);

assertEquals(
await userService.hashPassword("hunter1", "salt1"),
"4e3cff67fb50b608d58046330a2daea4c6c97e7b97b8ed8095bf95496fb85e61",
);
assertEquals(
await userService.hashPassword("hunter2", "salt1"),
"551127e9557988f8c6752c1776bbe77b0ab4415f7f3e1f0b90dd72bfc23076d6",
);
assertEquals(
await userService.hashPassword("hunter1", "salt2"),
"77b5f0f8e2d3c93fdb73ef0ea0a727ff86321b3585fdcd38c26bd60d376b6c1e",
);
});

test(userServiceTests, "getAuthenticated not implemented", async () => {
const result = userService.getAuthenticated(
"Kyle",
Expand All @@ -53,3 +29,44 @@ test(userServiceTests, "getAuthenticated not implemented", async () => {
"not implemented",
);
});

test("generateSalt", () => {
const salts = [
generateSalt(),
generateSalt(),
];
assertNotEquals(salts[0], salts[1]);
assertEquals(salts[0].length, 32);
assertEquals(salts[1].length, 32);
});

test("hashPassword", async () => {
const passwords = ["hunter1", "hunter2"];
const salts = [
"ba387b742a3e1917d084d067e3a65b63",
"f6f979051fadff4f12a87c99206cab14",
];
const result = hashPassword(passwords[0], salts[0]);
assertStrictEquals(Promise.resolve(result), result);
assertEquals(
await result,
"ef43ab3f512a1187e64f7595d1d0b5861f88498dc15362e27ff26b8bb23dd131",
);
assertEquals(
await hashPassword(passwords[0], salts[0]),
"ef43ab3f512a1187e64f7595d1d0b5861f88498dc15362e27ff26b8bb23dd131",
);
assertEquals(
await hashPassword(passwords[0], salts[1]),
"76b80d04d5d3de41c912378f05fab2570435855ea665da0710fc98efe62d4545",
);

assertEquals(
await hashPassword(passwords[1], salts[0]),
"02d3216c2cf31e04112c92955fc8352f2713905e8184b6375481b1d01a5358eb",
);
assertEquals(
await hashPassword(passwords[1], salts[1]),
"331b0dac7b339cf16de6be2166cc469b2e72b5088204b1f381e54dc94d92cc7c",
);
});

0 comments on commit 1ea0ebd

Please sign in to comment.