Skip to content

Commit

Permalink
fix: undo previous refactoring, and up version
Browse files Browse the repository at this point in the history
  • Loading branch information
markusberg committed Jun 22, 2020
1 parent 874c869 commit 0b30428
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 175 deletions.
22 changes: 1 addition & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,6 @@ or clone it from github:
$ git clone https://github.com/markusberg/ltpa.git
```

## API

This is the full API, but normally you'll only use a few of these methods. See examples below.

- `setSecrets(secrets: Secrets)`: Add your server secrets to the library, for later use in validation and signing of tokens
- `refresh(token: string, domain: string)`: Validate provided token, and return a fresh token
- `generateUserNameBuf(userName: string)`: Generate a userName Buffer. Currently hardcoded to CP-850, but the true char encoding is LMBCS
- `generate(userNameBuf: Buffer, domain: string)`: Generate a Base64-encoded Ltpa token
- `setValidity(seconds: number)`: Set how long a generated token is valid. Default is 5400 seconds (90 minutes).
- `setStrictExpirationValidation(strict: boolean)`: If set to true, token expiration validation will check the actual validation timestamp in the token instead of the calculated expiration. See the "Known Issues" section below.
- `setGracePeriod(seconds: number)`: Set the amount of time outside a ticket's validity that we will still accept it. This time is also added to the validity of tokens that are generated. Default is 300 seconds (5 minutes).
**NOTE:** since the grace period is added both on token generation, and during validation, the actual grace period is double what is set here.
- `getUserName(token: string)`: Retrieve the username as a `string` from the provided token. No validation of the token is performed
- `getUserNameBuf(token: string)`: Retrieve the username as a `Buffer` from the provided token. No validation of the token is performed
- `validate(token: string, domain: string)`: Validate provided token. Throws an error if validation fails

## Example 1

These examples are for [Express](https://expressjs.com/), but the functionality should be easy to adapt to [Koa](https://koajs.com/) or other frameworks.
Expand All @@ -61,7 +45,7 @@ ltpa.setSecrets({

/***
* Express Middleware
* Authorize user by verifying the provided LtpaToken cookie
* Authenticate user by verifying the provided LtpaToken cookie
*/
function mwLtpaAuth(req, res, next) {
try {
Expand Down Expand Up @@ -134,10 +118,6 @@ $ npm run test:watch

## Known issues

### Token validity

When validating token expiration, the library will only respect its internal `validity` setting, and will disregard the expiration-date setting in provided tokens. To force the library to use the actual timestamp in the token, use the setStrictExpirationValidation() method. This behaviour might change in version 2.

### Character set

The module only works with usernames containing characters in the `ibm850` codepage (basically Latin-1). The username in the token _should be_ encoded in an IBM proprietary format called `LMBCS` (Lotus Multi-Byte Character Set) for which I have found no javascript implementation. However, `LMBCS` is backwards compatible with `ibm850` for all characters in that codepage so if your usernames don't contain characters outside of `ibm850`, then you're good to go.
Expand Down
132 changes: 0 additions & 132 deletions Token.class.ts

This file was deleted.

89 changes: 70 additions & 19 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createHash } from "crypto"
import { decode, encode } from "iconv-lite"
import { Token } from "./Token.class"

export {
generate,
Expand Down Expand Up @@ -80,14 +80,30 @@ function generateUserNameBuf(userName: string): Buffer {
* @returns {string} The LtpaToken encoded as Base64
*/
function generate(userNameBuf: Buffer, domain: string, timeStart?: number) {
let token = new Token()
const start = timeStart ? timeStart : Math.floor(Date.now() / 1000)

token.timeCreation = start - gracePeriod
token.timeExpiration = start + validity + gracePeriod
token.username = userNameBuf
token.sign(ltpaSecrets[domain])
return token.getBuffer().toString("base64")
const timeCreation = (start - gracePeriod).toString(16)
const timeExpiration = (start + validity + gracePeriod).toString(16)

const size = userNameBuf.length + 40
const ltpaToken = Buffer.alloc(size)

ltpaToken.write("00010203", 0, 4, "hex")
ltpaToken.write(timeCreation, 4)
ltpaToken.write(timeExpiration, 12)
userNameBuf.copy(ltpaToken, 20)
const serverSecret = ltpaSecrets[domain]
ltpaToken.write(serverSecret, size - 20, 20, "base64")

const hash = createHash("sha1")
hash.update(ltpaToken)

// Paranoid overwrite of the server secret
ltpaToken.write("0123456789abcdefghij", size - 20, 20, "utf8")

// Append the token hash
ltpaToken.write(hash.digest("hex"), size - 20, 20, "hex")
return ltpaToken.toString("base64")
}

/***
Expand All @@ -99,6 +115,9 @@ function validate(token: string, domain: string): void {
/**
* Basic sanity checking of in-data
*/
if (!token || token.length === 0) {
throw new Error("No token provided")
}
if (!domain || domain.length === 0) {
throw new Error("No domain provided")
}
Expand All @@ -108,18 +127,50 @@ function validate(token: string, domain: string): void {
throw new Error("No such server secret exists")
}

const ltpaToken = new Token()
ltpaToken.parse(token)
ltpaToken.validateTimeCreation(gracePeriod)
const tokenSize = Buffer.byteLength(token, "base64")
const ltpaToken = Buffer.alloc(tokenSize, token, "base64")
if (ltpaToken.length < 41) {
// userName must be at least one character long
throw new Error("Ltpa Token too short")
}

/**
* Check time validity
*/
const timeCreation = parseInt(ltpaToken.toString("utf8", 4, 12), 16)
// we don't look at the expiration stored in the token, but calculate our own
const timeExpiration = parseInt(ltpaToken.toString("utf8", 12, 20), 16)
const now = Math.floor(Date.now() / 1000)

if (timeCreation - gracePeriod > now) {
throw new Error("Ltpa Token not yet valid")
}

const exp = strictExpirationValidation
? timeExpiration
: timeCreation + validity + gracePeriod * 2
// need to check two gracePeriods into the future because we add one to the beginning
if (exp < now) {
throw new Error("Ltpa Token has expired")
}

if (strictExpirationValidation) {
ltpaToken.validateTimeExpirationStrict()
} else {
ltpaToken.validateTimeExpiration(validity, gracePeriod)
/**
* Check version, and hash itself
*/
const version = ltpaToken.toString("hex", 0, 4)
if (version !== "00010203") {
throw new Error("Incorrect magic string")
}

ltpaToken.validateVersion()
ltpaToken.validateHash(serverSecret)
const signature = ltpaToken.toString("hex", ltpaToken.length - 20)
ltpaToken.write(serverSecret, ltpaToken.length - 20, 20, "base64")

const hash = createHash("sha1")
hash.update(ltpaToken)

if (hash.digest("hex") !== signature) {
throw new Error("Ltpa Token signature doesn't validate")
}
}

/***
Expand All @@ -128,9 +179,9 @@ function validate(token: string, domain: string): void {
* @returns {buffer} Buffer containing the encoded username
*/
function getUserNameBuf(token: string): Buffer {
const ltpaToken = new Token()
ltpaToken.parse(token)
return ltpaToken.username
const size = Buffer.byteLength(token, "base64")
const ltpaToken = Buffer.alloc(size, token, "base64")
return ltpaToken.slice(20, ltpaToken.length - 20)
}

/***
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ltpa",
"version": "1.1.1",
"version": "1.2.0",
"description": "Ltpa token generation and validation",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
3 changes: 2 additions & 1 deletion test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { expect } from "chai"
import * as ltpa from "../index"
import { Token } from "../Token.class"

const secrets = {
"example.com": "AAECAwQFBgcICQoLDA0ODxAREhM=",
Expand Down Expand Up @@ -113,6 +112,7 @@ describe("Ltpa", function () {
})
})

/* TODO: Use rewire to unit-test these parts
it("should validate the hashes", () => {
const tok = new Token()
knownTokens.forEach((token) => {
Expand All @@ -132,6 +132,7 @@ describe("Ltpa", function () {
)
})
})
*/
})

describe("token generation and refresh", () => {
Expand Down

0 comments on commit 0b30428

Please sign in to comment.