Skip to content

Commit

Permalink
feat: implement support for ibm852 codepage
Browse files Browse the repository at this point in the history
  • Loading branch information
markusberg-sectra committed Feb 14, 2024
1 parent 96fd6de commit e2106c7
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 47 deletions.
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.

## [2.0.0] - 2024-02-09

- Add support for codepage 852 enabling eastern european characters in usernames
- Drop support for Node.js versions below 18
- Ecmascript only
- Migrate to vitest and node:test for testing
Expand Down
78 changes: 39 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,33 +54,35 @@ These examples are for [Express](https://expressjs.com/), but the functionality
Add the dependency and create a simple middleware:

```javascript
let ltpa = require("ltpa")
ltpa.setSecrets({
"example.com": "AAECAwQFBgcICQoLDA0ODxAREhM=",
import { getUserName, refresh, setSecrets } from 'ltpa'
import { NextFunction, Request, Response } from 'express'

setSecrets({
'example.com': 'AAECAwQFBgcICQoLDA0ODxAREhM=',
})

/***
/**
* Express Middleware
* Authenticate user by verifying the provided LtpaToken cookie
*/
function mwLtpaAuth(req, res, next) {
export function mwLtpaAuth(req: Request, res: Response, next: NextFunction) {
try {
let ltpaToken = ltpa.refresh(req.cookies.LtpaToken, "example.com")
let newCookie =
"LtpaToken=" + ltpaToken + "; Path=/; Domain=" + "example.com"
res.setHeader("Set-Cookie", newCookie)
const ltpaToken = refresh(req.cookies.LtpaToken, 'example.com')
const newCookie =
'LtpaToken=' + ltpaToken + '; Path=/; Domain=' + 'example.com'
res.setHeader('Set-Cookie', newCookie)
next()
} catch (err) {
console.log(err)
res.status(401).json({ message: "Not authorized for this resource" })
res.status(401).json({ message: 'Not authorized for this resource' })
}
}

/***
/**
* Express route
*/
router.get("/testAuth", mwLtpaAuth, function (req, res) {
res.send("user is logged in as " + ltpa.getUserName(req.cookies.LtpaToken))
router.get('/testAuth', mwLtpaAuth, (req: Request, res: Response) => {
res.send('user is logged in as ' + getUserName(req.cookies.LtpaToken))
})
```

Expand All @@ -89,34 +91,32 @@ router.get("/testAuth", mwLtpaAuth, function (req, res) {
If you need to access a backend Domino database using a specific user account,
you can generate an LtpaToken for that account using the `generate` method:

```javascript
let ltpa = require("ltpa")
let rp = require("request-promise")
```typescript
import { Request, Response } from 'express'
import { generate, generateUserNameBuf, setSecrets } from 'ltpa'

ltpa.setSecrets({
"example.com": "AAECAwQFBgcICQoLDA0ODxAREhM=",
setSecrets({
'example.com': 'AAECAwQFBgcICQoLDA0ODxAREhM=',
})

router.get("/myDominoView", function (req, res) {
let userNameBuf = ltpa.generateUserNameBuf("Sysadmin Account")
let backendToken = ltpa.generate(userNameBuf, "example.com")

let dominoRequest = {
uri: "https://domino.example.com/api/data/collections/name/myDominoView",
method: "GET",
strictSSL: true,
timeout: 30000,
headers: {
Cookie: "LtpaToken=" + backendToken,
},
}
router.get('/myDominoView', async (req: Request, res: Response) => {
const userNameBuf = generateUserNameBuf('CN=Sysadmin Account,O=Example Inc')
const backendToken = generate(userNameBuf, 'example.com')

const url = new URL(
'/api/data/collections/name/myDominoView',
'https://domino.example.com/',
)
const headers = { Cookie: `LtpaToken=${backendToken}` }

rp(dominoRequest)
.then((response) => res.json(response))
.catch((err) => {
console.log(err)
res.status(500).send(err)
})
try {
const response = await fetch(url, { headers })
const json = await response.json()
res.json(json)
} catch (err) {
console.error(err)
res.status(500).send(err)
}
})
```

Expand All @@ -138,9 +138,9 @@ $ npm run test:watch

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
### Character sets

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.
The module only works with usernames containing characters in the `ibm850`, and `ibm852` codepages (this covers most of Europe). The username in the token is encoded in an old IBM/Lotus format called [`LMBCS` (Lotus Multi-Byte Character Set)](https://en.wikipedia.org/wiki/Lotus_Multi-Byte_Character_Set) for which I have found no javascript implementation.

### LTPA1 only

Expand Down
48 changes: 42 additions & 6 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ let validity = 5400
let gracePeriod = 300
let strictExpirationValidation = false

/**
* Special handling of Codepage 852
*/
const ibm852Chars =
'ÇüéâäůćçłëŐőîŹÄĆÉĹĺôöĽľŚśÖÜŤťŁčáíóúĄąŽžĘ꬟ȺÁÂĚŞŻżĂăđĐĎËďŇÍÎěŢŮÓßÔŃńňŠšŔÚŕŰýÝţűŘř'.split(
'',
)
const buf852 = Buffer.from([0x06])

/***
* Set how long a generated token is valid. Default is 5400 seconds (90 minutes)
* @param {number} seconds Number of seconds that tokens are valid
Expand Down Expand Up @@ -65,11 +74,20 @@ function setSecrets(secrets: Secrets) {
/***
* Generate a userName Buffer. Currently hardcoded to CP-850, but the
* true char encoding is LMBCS
* @param {string} userName The username to be converted to a CP-850 buffer
* @param {string} username The username to be converted to a CP-850 buffer
* @returns {Buffer} Username encoded in CP-850 and stuffed into a Buffer
*/
function generateUserNameBuf(userName: string): Buffer {
return iconv.encode(userName, 'ibm850')
function generateUserNameBuf(username: string): Buffer {
const bufUsername = username.split('').reduce((acc, char) => {
if (ibm852Chars.includes(char)) {
const bufChar = iconv.encode(char, 'ibm852')
return Buffer.concat([acc, buf852, bufChar])
}
const bufChar = iconv.encode(char, 'ibm850')
return Buffer.concat([acc, bufChar])
}, Buffer.from(''))

return bufUsername
}

/***
Expand All @@ -79,7 +97,11 @@ function generateUserNameBuf(userName: string): Buffer {
* @param {number} timeStart Timestamp (seconds) for when the token validity should start. Default: now
* @returns {string} The LtpaToken encoded as Base64
*/
function generate(userNameBuf: Buffer, domain: string, timeStart?: number) {
function generate(
userNameBuf: Buffer,
domain: string,
timeStart?: number,
): string {
const start = timeStart ? timeStart : Math.floor(Date.now() / 1000)

const timeCreation = (start - gracePeriod).toString(16)
Expand Down Expand Up @@ -181,7 +203,7 @@ function validate(token: string, domain: string): void {
function getUserNameBuf(token: string): Buffer {
const size = Buffer.byteLength(token, 'base64')
const ltpaToken = Buffer.alloc(size, token, 'base64')
return ltpaToken.slice(20, ltpaToken.length - 20)
return ltpaToken.subarray(20, ltpaToken.length - 20)
}

/***
Expand All @@ -190,7 +212,21 @@ function getUserNameBuf(token: string): Buffer {
* @returns {string} Username as a UTF-8 string
*/
function getUserName(token: string): string {
return iconv.decode(getUserNameBuf(token), 'ibm850')
const bufUsername = getUserNameBuf(token)
let username: string[] = []
for (let i = 0; i < bufUsername.length; i++) {
const char = bufUsername.subarray(i, i + 1)
if (char.equals(buf852)) {
const utf8 = iconv.decode(bufUsername.subarray(i + 1, i + 2), 'ibm852')
username.push(utf8)
i++
} else {
const utf8 = iconv.decode(char, 'ibm850')
username.push(utf8)
}
}

return username.join('')
}

/***
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"name": "ltpa",
"version": "2.0.0",
"version": "2.0.0-alpha.0",
"description": "Ltpa token generation and validation",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"exports": "./dist/index.js",
"scripts": {
"build": "rm -fr dist && tsc",
"build": "npm run clean && tsc",
"clean": "rm -fr dist",
"watch": "npm run clean && tsc -w",
"prepublishOnly": "npm run build",
"test": "vitest run --coverage",
"test:watch": "vitest watch",
Expand Down
26 changes: 26 additions & 0 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,30 @@ describe('Ltpa', function () {
)
})
})

describe('codepage handling', () => {
it('should be able to convert an ascii username and back again', () => {
const username = 'my test username'
const buf = ltpa.generateUserNameBuf(username)
const token = ltpa.generate(buf, 'example.com')
const backAgain = ltpa.getUserName(token)
assert.equal(username, backAgain)
})

it('should be able to convert an ibm852 username and back again', () => {
const username = 'Łuczak'
const buf = ltpa.generateUserNameBuf(username)
const token = ltpa.generate(buf, 'example.com')
const backAgain = ltpa.getUserName(token)
assert.equal(username, backAgain)
})

it('should be able to handle a username containing both ibm850 and ibm852 characters', () => {
const username = 'Måns Östen Łučzak'
const buf = ltpa.generateUserNameBuf(username)
const token = ltpa.generate(buf, 'example.com')
const backAgain = ltpa.getUserName(token)
assert.equal(username, backAgain)
})
})
})

0 comments on commit e2106c7

Please sign in to comment.