Skip to content

Commit

Permalink
Merge pull request #411 from Infisical/revised-service-token-docs
Browse files Browse the repository at this point in the history
Add read/write support for service tokens and update CRUD examples in docs to use service tokens
  • Loading branch information
dangtony98 authored Mar 7, 2023
2 parents 1f316a0 + 61d4da4 commit 4edfc1e
Show file tree
Hide file tree
Showing 26 changed files with 1,842 additions and 605 deletions.
968 changes: 810 additions & 158 deletions backend/spec.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/src/controllers/v2/secretsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}
}
*/

const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id);

Expand Down
37 changes: 34 additions & 3 deletions backend/src/controllers/v2/serviceTokenDataController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,35 @@ import { ABILITY_READ } from '../../variables/organization';
* @param res
* @returns
*/
export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData);
export const getServiceTokenData = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return Infisical Token data'
#swagger.description = 'Return Infisical Token data'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"serviceTokenData": {
"type": "object",
$ref: "#/components/schemas/ServiceTokenData",
"description": "Details of service token"
}
}
}
}
}
}
*/

return res.status(200).json(req.serviceTokenData);
}

/**
* Create new service token data for workspace with id [workspaceId] and
Expand All @@ -28,6 +56,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => res.st
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceToken, serviceTokenData;

try {
const {
name,
Expand All @@ -36,7 +65,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
encryptedKey,
iv,
tag,
expiresIn
expiresIn,
permissions
} = req.body;

const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
Expand All @@ -59,7 +89,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
secretHash,
encryptedKey,
iv,
tag
tag,
permissions
}).save();

// return service token data without sensitive data
Expand Down
6 changes: 5 additions & 1 deletion backend/src/helpers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import bcrypt from 'bcrypt';
import {
IUser,
User,
ServiceTokenData,
APIKeyData
Expand Down Expand Up @@ -148,7 +149,10 @@ const getAuthSTDPayload = async ({

serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag').populate('user');
.select('+encryptedKey +iv +tag')
.populate<{user: IUser}>('user');

if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });

} catch (err) {
throw UnauthorizedRequestError({
Expand Down
21 changes: 18 additions & 3 deletions backend/src/middleware/requireAuth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { User, ServiceTokenData } from '../models';
import {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload
} from '../helpers/auth';
import {
UnauthorizedRequestError
} from '../utils/errors';

declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
Expand All @@ -25,9 +27,11 @@ declare module 'jsonwebtoken' {
* @returns
*/
const requireAuth = ({
acceptedAuthModes = ['jwt']
acceptedAuthModes = ['jwt'],
requiredServiceTokenPermissions = []
}: {
acceptedAuthModes: string[];
requiredServiceTokenPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes]
Expand All @@ -38,11 +42,22 @@ const requireAuth = ({
});

// attach auth payloads
let serviceTokenData: any;
switch (authTokenType) {
case 'serviceToken':
req.serviceTokenData = await getAuthSTDPayload({
serviceTokenData = await getAuthSTDPayload({
authTokenValue
});

requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => {
if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) {
return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' }));
}
});

req.serviceTokenData = serviceTokenData;
req.user = serviceTokenData?.user;

break;
case 'apiKey':
req.user = await getAuthAPIKeyPayload({
Expand Down
2 changes: 1 addition & 1 deletion backend/src/middleware/requireSecretsAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const requireSecretsAuth = ({
// case: validate 1 secret
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: req.body.secrets.id
secretIds: [req.body.secrets.id]
});
} else if (Array.isArray(req.body.secretIds)) {
secrets = await validateSecrets({
Expand Down
6 changes: 3 additions & 3 deletions backend/src/middleware/requireWorkspaceAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const requireWorkspaceAuth = ({
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { workspaceId } = req[location];

if (req.user) {
// case: jwt auth
const membership = await validateMembership({
Expand All @@ -32,11 +32,11 @@ const requireWorkspaceAuth = ({

req.membership = membership;
}

if (
req.serviceTokenData
&& req.serviceTokenData.workspace !== workspaceId
&& req.serviceTokenData.environment !== req.query.environment
&& req.serviceTokenData.environment !== req.body.environment
) {
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
}
Expand Down
8 changes: 7 additions & 1 deletion backend/src/models/serviceTokenData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { Schema, model, Types } from 'mongoose';
export interface IServiceTokenData {
name: string;
workspace: Types.ObjectId;
environment: string; // TODO: adapt to upcoming environment id
environment: string;
user: Types.ObjectId;
expiresAt: Date;
secretHash: string;
encryptedKey: string;
iv: string;
tag: string;
permissions: string[];
}

const serviceTokenDataSchema = new Schema<IServiceTokenData>(
Expand Down Expand Up @@ -51,6 +52,11 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
tag: {
type: String,
select: false
},
permissions: {
type: [String],
enum: ['read', 'write'],
default: ['read']
}
},
{
Expand Down
17 changes: 10 additions & 7 deletions backend/src/routes/v2/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
router.post(
'/batch',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['read', 'write']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
Expand Down Expand Up @@ -99,7 +100,8 @@ router.post(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
Expand All @@ -115,7 +117,8 @@ router.get(
query('tagSlugs'),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['read']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
Expand Down Expand Up @@ -154,7 +157,8 @@ router.patch(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
Expand Down Expand Up @@ -182,7 +186,8 @@ router.delete(
.isEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
Expand All @@ -192,5 +197,3 @@ router.delete(

export default router;



23 changes: 16 additions & 7 deletions backend/src/routes/v2/serviceTokenData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,22 @@ router.post(
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('name').exists().trim(),
body('workspaceId'),
body('environment'),
body('encryptedKey'),
body('iv'),
body('tag'),
body('expiresIn'), // measured in ms
body('name').exists().isString().trim(),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('encryptedKey').exists().isString().trim(),
body('iv').exists().isString().trim(),
body('tag').exists().isString().trim(),
body('expiresIn').exists().isNumeric(), // measured in ms
body('permissions').isArray({ min: 1 }).custom((value: string[]) => {
const allowedPermissions = ['read', 'write'];
const invalidValues = value.filter((v) => !allowedPermissions.includes(v));
if (invalidValues.length > 0) {
throw new Error(`permissions contains invalid values: ${invalidValues.join(', ')}`);
}

return true
}),
validateRequest,
serviceTokenDataController.createServiceTokenData
);
Expand Down
17 changes: 17 additions & 0 deletions backend/swagger/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,23 @@ const generateOpenAPISpec = async () => {
secretValueCiphertext: '',
secretValueIV: '',
secretValueTag: '',
},
ServiceTokenData: {
_id: '',
name: '',
workspace: '',
environment: '',
user: {
_id: '',
firstName: '',
lastName: ''
},
expiresAt: '2023-01-13T14:16:12.210Z',
encryptedKey: '',
iv: '',
tag: '',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
}
}
};
Expand Down
4 changes: 4 additions & 0 deletions docs/api-reference/endpoints/service-tokens/get.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: "Get"
openapi: "GET /api/v2/service-token/"
---
24 changes: 17 additions & 7 deletions docs/api-reference/overview/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@
title: "Authentication"
---

To authenticate requests with Infisical, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. You can obtain an API key in User Settings > API Keys
To authenticate requests with Infisical, you can either use an API Key or [Infisical Token](../../../getting-started/dashboard/token); certain endpoints will accept either one or both.
- API Key: This general-purpose authentication token provides user access to most endpoints in this reference.
- [Infisical Token](../../../getting-started/dashboard/token): This authentication token (also referred to as the service token) is scoped to a specific project and environment and used for CRUD secret operations.

<AccordionGroup>
<Accordion title="API Key">
To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform.

You can obtain an API key in User Settings > API Keys

![API key dashboard](../../images/api-key-dashboard.png)
![API key in personal settings](../../images/api-key-settings.png)
![Adding an API key](../../images/api-key-add.png)
</Accordion>
<Accordion title="Infisical Token">
To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer st.<rest_of_your_infisical_token>`.

You can obtain an Infisical Token in Project Settings > Service Tokens.

<Info>
It's important to keep your API key secure, as it grants access to your
secrets in Infisical. For added security, set a reasonable expiration time and
rotate your API key on a regular basis.
</Info>
![token add](../../images/project-token-add.png)
</Accordion>
</AccordionGroup>
Loading

0 comments on commit 4edfc1e

Please sign in to comment.