Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add read/write support for service tokens and update CRUD examples in docs to use service tokens #411

Merged
merged 5 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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