@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
190 lines (169 loc) • 8.19 kB
JavaScript
const cds = require('../../../index.js')
const LOG = cds.log('auth')
const {
createSecurityContext,
Token,
IdentityService,
XsuaaService,
XsuaaToken,
errors: { ValidationError }
} = require('./xssec')
module.exports = function ias_auth(config) {
// cds.env.requires.auth.known_claims is not an official config!
const { kind, credentials, config: serviceConfig = {}, known_claims = KNOWN_CLAIMS, xsuaa = 'xsuaa' } = config
const skipped_attrs = known_claims.reduce((a, x) => ((a[x] = 1), a), {})
if (!credentials)
cds.error(`Authentication kind "${kind}" configured, but no IAS instance bound to application. ` +
'Either bind an IAS instance, or switch to an authentication kind that does not require a binding.')
// enable signature cache by default
serviceConfig.validation ??= {}
if (!('signatureCache' in serviceConfig.validation)) serviceConfig.validation.signatureCache = { enabled: true }
// activate decode cache if not already done or explicitely disabled by setting Token.decodeCache to false or undefined
if (Token.decodeCache === null) Token.enableDecodeCache()
const auth_service = new IdentityService(credentials, serviceConfig)
const user_factory = get_user_factory(credentials, skipped_attrs)
/*
* re validation:
* if the request goes to the cert url, then we should validate the token.
* however, this requires header "x-forwarded-client-cert" which requires additional configuration in the approuter ("forwardAuthCertificates: true").
* also, we currently get the non-cert route attached to the application as well (-> adjust "cds add mta"?), for which validation would always fail.
* by default, the approuter is configured to use the non-cert route ("url: ~{srv-url}" instead of "url: ~{srv-cert-url}").
* if the developer explicitely changes to the cert route, then we can expect him/her to also configure cert forwarding.
* hence, if there is no explicit validation configuration by the app, we can and should create a service with validation enabled and use if for the cert route.
* this way, we validate if possible with the least amount of custom configuration.
*/
const should_validate =
process.env.VCAP_APPLICATION &&
JSON.parse(process.env.VCAP_APPLICATION).application_uris?.some(uri => uri.match(/\.cert\.|\.mesh\.cf\./))
const validation_configured = serviceConfig.validation?.x5t?.enabled != null || serviceConfig.validation?.proofToken?.enabled != null
let validating_auth_service
if (should_validate && !validation_configured) {
const _serviceConfig = { ...serviceConfig }
_serviceConfig.validation = { x5t: { enabled: true }, proofToken: { enabled: true } }
validating_auth_service = new IdentityService(credentials, _serviceConfig)
}
// xsuaa fallback allows to also accept XSUAA tokens during migration to IAS
// automatically enabled if xsuaa credentials are available
let xsuaa_service, xsuaa_user_factory
if (cds.env.requires[xsuaa]?.credentials) {
const { credentials: xsuaa_credentials, config: xsuaa_serviceConfig = {} } = cds.env.requires[xsuaa]
xsuaa_service = new XsuaaService(xsuaa_credentials, xsuaa_serviceConfig)
const get_xsuaa_user_factory = require('./jwt-auth')._get_user_factory
xsuaa_user_factory = get_xsuaa_user_factory(xsuaa_credentials, xsuaa_credentials.xsappname, 'xsuaa')
}
return async function ias_auth(req, _, next) {
if (!req.headers.authorization) return next()
try {
const _auth_service =
validating_auth_service && req.hostname.match(/\.cert\.|\.mesh\.cf\./) ? validating_auth_service : auth_service
const securityContext = await createSecurityContext(xsuaa_service ? [_auth_service, xsuaa_service] : _auth_service, { req })
const ctx = cds.context
ctx.user = securityContext.token instanceof XsuaaToken ? xsuaa_user_factory(securityContext) : user_factory(securityContext)
ctx.tenant = securityContext.token.getZoneId()
// REVISIT: remove compat in cds^10
Object.defineProperty(req, 'authInfo', {
get() {
cds.utils.deprecated({ kind: 'API', old: 'cds.context.http.req.authInfo', use: 'cds.context.user.authInfo' })
return securityContext
}
})
} catch (e) {
if (e instanceof ValidationError) {
if (e.token?.payload) e.token_payload = e.token.payload
LOG.warn('Unauthenticated request:', e)
return next(401)
}
LOG.error('Error while authenticating user:', e)
return next(500)
}
next()
}
}
function get_user_factory(credentials, skipped_attrs) {
return function user_factory(securityContext) {
const tokenInfo = securityContext.token
const payload = tokenInfo.getPayload()
/*
* NOTE:
* for easier migration, xssec will offer IAS without policies via so-called XsuaaFallback.
* in that case, we would need to add the roles here based on the tokenInfo (similar to xsuaa-auth).
* however, it is not yet clear where the roles will be stored in IAS' tokenInfo object.
* further, stakeholders would need to configure the "extension" programmatically (e.g., in a custom server.js).
*/
const clientid = tokenInfo.getClientId()
if (clientid === payload.sub) {
//> grant_type === client_credentials or x509
const roles = { 'system-user': 1 }
if (Array.isArray(payload.ias_apis)) payload.ias_apis.forEach(r => (roles[r] = 1))
if (clientid === credentials.clientid) roles['internal-user'] = 1
else delete roles['internal-user']
const user = new cds.User({ id: 'system', roles, authInfo: securityContext })
// REVISIT: remove compat in cds^10
Object.defineProperty(user, 'tokenInfo', {
get() {
// prettier-ignore
cds.utils.deprecated({ kind: 'API', old: 'cds.context.user.tokenInfo', use: 'cds.context.user.authInfo.token' })
return securityContext.token
}
})
return user
}
// add all unknown attributes to req.user.attr in order to keep public API small
const attr = {}
for (const key in payload) {
if (key in skipped_attrs)
continue // REVISIT: Why do we need to do that?
else attr[key] = payload[key]
}
// REVISIT: just don't such things, please! -> We're just piling up tech dept through tons of unoficcial long tail APIs like that!
// REVISIT: looks like wrong direction to me, btw
// same api as xsuaa-auth for easier migration
if (attr.user_name) attr.logonName = attr.user_name
if (attr.given_name) attr.givenName = attr.given_name
if (attr.family_name) attr.familyName = attr.family_name
const user = new cds.User({ id: payload.sub, attr, authInfo: securityContext })
// REVISIT: remove compat in cds^10
Object.defineProperty(user, 'tokenInfo', {
get() {
cds.utils.deprecated({ kind: 'API', old: 'cds.context.user.tokenInfo', use: 'cds.context.user.authInfo.token' })
return securityContext.token
}
})
return user
}
}
// REVISIT: Why do we need to know and do that?
const KNOWN_CLAIMS = Object.values({
/*
* JWT claims (https://datatracker.ietf.org/doc/html/rfc7519#section-4)
*/
ISSUER: 'iss',
SUBJECT: 'sub',
AUDIENCE: 'aud',
EXPIRATION_TIME: 'exp',
NOT_BEFORE: 'nbf',
ISSUED_AT: 'iat',
JWT_ID: 'jti',
/*
* TokenClaims (com.sap.cloud.security.token.TokenClaims)
*/
// ISSUER: "iss", //> already in JWT claims
IAS_ISSUER: 'ias_iss',
// EXPIRATION: "exp", //> already in JWT claims
// AUDIENCE: "aud", //> already in JWT claims
// NOT_BEFORE: "nbf", //> already in JWT claims
// SUBJECT: "sub", //> already in JWT claims
// USER_NAME: 'user_name', //> do not exclude
// GIVEN_NAME: 'given_name', //> do not exclude
// FAMILY_NAME: 'family_name', //> do not exclude
// EMAIL: 'email', //> do not exclude
SAP_GLOBAL_SCIM_ID: 'scim_id',
SAP_GLOBAL_USER_ID: 'user_uuid', //> exclude for now
SAP_GLOBAL_ZONE_ID: 'zone_uuid',
// GROUPS: 'groups', //> do not exclude
AUTHORIZATION_PARTY: 'azp',
CNF: 'cnf',
CNF_X5T: 'x5t#S256',
// own
APP_TENANT_ID: 'app_tid'
})