integreat-io/integreat

View on GitHub
src/authenticators/options.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import { pathGetter } from 'map-transform'
import { isObject } from '../utils/is.js'
import type { Authenticator, Action } from '../types.js'
import type { AuthOptions, Authentication } from '../service/types.js'
 
export interface ValidateOptions extends AuthOptions {
identId?: string
}
 
// `pathGetter` is requiring state, so give the minimal state needed
const state = { context: [], value: undefined }
 
const compareValues = (expected: unknown | unknown[], value: unknown) =>
expected !== undefined && Array.isArray(expected)
? expected.includes(value)
: expected === value
 
const getIsMatchCount = (matches: [boolean, boolean][]) =>
matches.filter(([match]) => !!match).length
 
const getHasPropsCount = (matches: [boolean, boolean][]) =>
matches.filter(([, exists]) => !!exists).length
 
// Returns a tuple where the first boolean is `true` if the entries in `options`
// are all present with the expected values in the given action. The second
// boolean is `true` if any of the entries in `options` are present in the given
// action, even though they might not have the expected values.
function validateOptions(options: AuthOptions, action: Action | null) {
if (action === null) {
// There is no action, so we don't have a match and none of the expected
// props are present
return [false, false] // [isMatch, hasProps]
}
 
// Extract the paths and the expected values from the options object
const pathAndExpectedArr = Object.entries(options)
if (pathAndExpectedArr.length === 0) {
// This is a match, although none of the expected props are present
return [true, false] // [isMatch, hasProps]
}
 
// Extract the values at the paths from the action, and compare them to the
// expected values. `matches` wil be an array of [isMatch, hasProp] tuples for
// each path.
const matches: [boolean, boolean][] = pathAndExpectedArr.map(function ([
path,
expected,
]) {
const value = pathGetter(path)(action, state)
return [compareValues(expected, value), value !== undefined] // [isMatch, hasProp]
})
 
// Count up the matches to return the expected flags
return [
getIsMatchCount(matches) === pathAndExpectedArr.length, // isMatch
getHasPropsCount(matches) > 0, // hasProps
]
}
 
/**
* The options authenticator. Will always be authenticated, and will return the
* given options it is given as authentication.
*/
const optionsAuth: Authenticator = {
/**
* Authenticate and return authentication object if authentication was
* successful.
* Would normaly perform an authentication request and return the token
* received, but in optionsAuth the given options object is returned as the
* authentication object.
*/
async authenticate(options) {
return { status: 'granted', ...options }
},
 
/**
* Check whether we've already ran authentication.
* In the optionsAuth, this will always be true, as no authentication is
* really necessary.
*/
isAuthenticated(authentication, _options, _action) {
return !!(authentication && authentication.status === 'granted')
},
 
/**
* Validate authentication object.
* The options authenticator will check that all the properties of the options
* object (except `identId`) are present in the given action. If so, an ident
* with the id provided in the `identId` option is returned. Otherwise, an
* error response is returned.
*/
async validate(_authentication, options: ValidateOptions | null, action) {
const { identId, ...authOptions } = options || {}
 
const [isValid, hasProps] = validateOptions(authOptions, action)
if (isValid) {
return { status: 'ok', access: { ident: { id: identId } } }
} else {
// The action was invalid, so return an error response. If the action had
// none of the expected props, we return a noaccess error, as this means
// no authentication was provided.
return hasProps
? {
status: 'autherror',
error: 'Invalid credentials',
reason: 'invalidauth',
}
: {
status: 'noaccess',
error: 'Authentication required',
reason: 'noauth',
}
}
},
 
authentication: {
/**
* Return an object with the information needed for authenticated requests
* with this strategy.
* For OptionsStrategy, this will simply be the options object given on
* creation.
*/
Similar blocks of code found in 2 locations. Consider refactoring.
asObject(authentication: Authentication | null): Record<string, unknown> {
if (isObject(authentication)) {
const { status, ...options } = authentication
if (status === 'granted') {
return options
}
}
return {}
},
 
/**
* Return a headers object with the headers needed for authenticated
* requests with this strategy. For OptionsStrategy, this will simply be the
* options object given on creation.
*/
Similar blocks of code found in 2 locations. Consider refactoring.
asHttpHeaders(
authentication: Authentication | null,
): Record<string, unknown> {
if (isObject(authentication)) {
const { status, ...options } = authentication
if (status === 'granted') {
return options
}
}
return {}
},
},
}
 
export default optionsAuth