Skip to content

Commit

Permalink
Merge pull request #11 from EATSTEAK/feat/auth-user
Browse files Browse the repository at this point in the history
`auth`, `user` 엔드포인트 구현
  • Loading branch information
EATSTEAK authored Aug 4, 2022
2 parents 59da0bb + 7411c6f commit 36ba0a9
Show file tree
Hide file tree
Showing 18 changed files with 660 additions and 406 deletions.
27 changes: 11 additions & 16 deletions packages/server/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ paths:
summary: 로그아웃
description: '로그아웃을 위한 엔드포인트입니다. 로그아웃 이후에는 클라이언트에서 토큰을 폐기해 주세요.'
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
responses:
'200':
description: OK
Expand Down Expand Up @@ -108,7 +108,7 @@ paths:
schema:
type: boolean
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
responses:
'200':
description: OK
Expand Down Expand Up @@ -192,7 +192,7 @@ paths:
summary: 사물함 대여(일반 학부생용)
description: 로그인된 학부생 명의로 사물함을 대여합니다.
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
requestBody:
content:
application/json:
Expand Down Expand Up @@ -245,7 +245,7 @@ paths:
summary: 사용자 정보 열람
description: 사용자의 정보를 열람합니다.
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
parameters:
- name: id
in: query
Expand Down Expand Up @@ -284,7 +284,7 @@ paths:
summary: 사용자 정보 수정
description: 사용자의 정보를 수정합니다.
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
requestBody:
content:
application/json:
Expand Down Expand Up @@ -322,7 +322,7 @@ paths:
summary: 사용자 정보 삭제
description: 사용자의 정보를 삭제합니다.
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
requestBody:
content:
application/json:
Expand Down Expand Up @@ -360,13 +360,8 @@ paths:
summary: 사용자 목록 열람
description: 사용자 목록을 열람합니다.
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
parameters:
- name: department
in: query
required: true
schema:
$ref: '#/components/schemas/DepartmentEnum'
- name: starts
in: query
schema:
Expand Down Expand Up @@ -405,7 +400,7 @@ paths:
summary: 사용자 일괄 삽입
description: 사용자를 일괄 삽입합니다.
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
requestBody:
content:
application/json:
Expand Down Expand Up @@ -443,7 +438,7 @@ paths:
summary: 사용자 일괄 삭제
description: 사용자를 일괄 삭제합니다.
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
requestBody:
content:
application/json:
Expand Down Expand Up @@ -481,7 +476,7 @@ paths:
summary: 전체 설정 열람
description: 서비스의 전체 설정을 열람합니다.
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
responses:
'200':
description: OK
Expand Down Expand Up @@ -519,7 +514,7 @@ paths:
summary: 설정 수정
description: 서비스 혹은 학부의 설정을 수정합니다.
security:
- UserSessionAuth: []
- UserSessionAuth: [ ]
requestBody:
content:
application/json:
Expand Down
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"devDependencies": {
"@types/aws-lambda": "^8.10.101",
"@types/jsonwebtoken": "^8.5.8",
"@types/lockerweb": "link:..\\types",
"@typescript-eslint/eslint-plugin": "^5.30.0",
"@typescript-eslint/parser": "^5.30.0",
"aws-sdk": "^2.1166.0",
Expand Down
110 changes: 66 additions & 44 deletions packages/server/src/auth/data.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,73 @@
import type { UpdateItemInput } from "aws-sdk/clients/dynamodb";
import { adminId, dynamoDB, TableName } from "../common";
import { UnauthorizedError } from "../error";
import type { GetItemInput, UpdateItemInput } from 'aws-sdk/clients/dynamodb';
import { UnauthorizedError } from '../util/error';
import { adminId, dynamoDB, TableName } from '../util/database';
import { fromUserDao } from '../user/data';

/* ISSUE/REVOKE TOKEN */

export const revokeToken = async function(
id: string,
token: string
id: string,
token: string
): Promise<{ accessToken: string }> {
const req: UpdateItemInput = {
TableName,
Key: { id: { S: id } },
UpdateExpression: 'REMOVE accessToken',
ConditionExpression: 'accessToken = :token',
ExpressionAttributeValues: {
':token': { S: token }
},
ReturnValues: 'UPDATED_OLD'
};
const res = await dynamoDB.updateItem(req).promise();
if (res.Attributes.hasOwnProperty('accessToken')) {
return { accessToken: token };
} else {
throw new UnauthorizedError();
}
const req: UpdateItemInput = {
TableName,
Key: { type: { S: 'user' }, id: { S: `${id}` } },
UpdateExpression: 'REMOVE aT',
ConditionExpression: 'aT = :token',
ExpressionAttributeValues: {
':token': { S: token }
},
ReturnValues: 'UPDATED_OLD'
};
const res = await dynamoDB.updateItem(req).promise();
if (res.Attributes.hasOwnProperty('aT')) {
return { accessToken: token };
} else {
throw new UnauthorizedError();
}
};

export const issueToken = async function(
id: string,
token: string
id: string,
token: string
): Promise<{ id: string; expires: number }> {
const expires = Date.now() + 3600 * 1000;
const req: UpdateItemInput = {
TableName,
Key: { id: { S: id } },
UpdateExpression: 'SET accessToken = :token, expiresOn = :expiresOn',
ExpressionAttributeValues: {
':token': { S: token },
':expiresOn': { N: `${expires}` }
},
ReturnValues: 'UPDATED_NEW'
};
if (id !== adminId) {
req.ConditionExpression = 'attribute_exists(is_admin) OR attribute_exists(department)';
}
const res = await dynamoDB.updateItem(req).promise();
if (res.Attributes.hasOwnProperty('accessToken')) {
return { id, expires };
} else {
throw new UnauthorizedError('Unauthorized', { id, expires });
}
};
const expires = Date.now() + 3600 * 1000 * 24;
const req: UpdateItemInput = {
TableName,
Key: { type: { S: 'user' }, id: { S: `${id}` } },
UpdateExpression: 'SET aT = :token, eO = :expiresOn',
...(id !== adminId && { ConditionExpression: 'attribute_exists(d)' }),
ExpressionAttributeValues: {
':token': { S: token },
':expiresOn': { N: `${expires}` }
},
ReturnValues: 'UPDATED_NEW'
};
const res = await dynamoDB.updateItem(req).promise();
if (res.Attributes.hasOwnProperty('aT')) {
return { id, expires };
} else {
throw new UnauthorizedError('Unauthorized', { id, expires });
}
};

export async function assertAccessible(id: string, token: string, adminOnly = false): Promise<User> {
const authReq: GetItemInput = {
TableName,
Key: {
module: { S: 'user' },
dataId: {
S: `${id}`
}
}
};
const authRes = await dynamoDB.getItem(authReq).promise();
if (
authRes.Item.id.S !== `${id}` ||
authRes.Item.aT?.S !== token ||
(adminOnly && authRes.Item.iA?.BOOL !== true && id !== adminId)
) {
throw new UnauthorizedError('Unauthorized');
}
return fromUserDao(authRes.Item as unknown as UserDao);
}
45 changes: 25 additions & 20 deletions packages/server/src/auth/handler/logout.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import type { APIGatewayProxyHandler } from "aws-lambda";
import * as jwt from "jsonwebtoken";
import { JWT_SECRET } from "../../env";
import type { JwtPayload } from "jsonwebtoken";
import { revokeToken } from "../data";
import { createResponse } from "../../common";
import type { APIGatewayProxyHandler } from 'aws-lambda';
import type { JwtPayload } from 'jsonwebtoken';
import * as jwt from 'jsonwebtoken';
import { JWT_SECRET } from '../../env';
import { assertAccessible, revokeToken } from '../data';
import { createResponse } from '../../common';
import { ResponsibleError } from '../../util/error';

export const logoutHandler: APIGatewayProxyHandler = async (event) => {
const token = (event.headers.Authorization ?? '').replace('Bearer ', '');
try {
const payload = jwt.verify(token, JWT_SECRET) as JwtPayload;
const res = await revokeToken(payload.aud as string, token);
return createResponse(200, { success: true, ...res });
} catch (err) {
const res = {
success: false,
token,
error: 401,
error_description: 'Unauthorized'
};
return createResponse(401, res);
}
const token = (event.headers.Authorization ?? '').replace('Bearer ', '');
try {
const payload = jwt.verify(token, JWT_SECRET) as JwtPayload;
await assertAccessible(payload.aud as string, token);
const res = await revokeToken(payload.aud as string, token);
return createResponse(200, { success: true, ...res });
} catch (err) {
if (err instanceof ResponsibleError) {
return err.response();
}
const res = {
success: false,
token,
error: 401,
error_description: 'Unauthorized'
};
return createResponse(401, res);
}
};
18 changes: 10 additions & 8 deletions packages/server/src/auth/handler/ssu_login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import type { APIGatewayProxyHandler } from 'aws-lambda';
import * as jwt from 'jsonwebtoken';
import { JWT_SECRET } from '../../env';
import { createResponse } from '../../common';
import { ResponsibleError, UnauthorizedError } from '../../error';
import { issueToken } from "../data";
import { ResponsibleError, UnauthorizedError } from '../../util/error';
import { issueToken } from '../data';

function requestBody(result: string): Promise<string> {
return new Promise((resolve, reject) => {
https
.get(`https://canvas.ssu.ac.kr/learningx/login/from_cc?result=${result}`, (res) => {
let body = '';
res.on('data', function (chunk) {
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function () {
res.on('end', function() {
resolve(body);
});
})
Expand Down Expand Up @@ -46,10 +46,12 @@ export const ssuLoginHandler: APIGatewayProxyHandler = async (event) => {
const left = Math.floor((issued.expires - Date.now()) / 1000);
const res = {
success: true,
id,
access_token: accessToken,
token_type: 'Bearer',
expires_in: left
result: {
id,
accessToken,
tokenType: 'Bearer',
expiresIn: left
}
};
return createResponse(200, { success: true, ...res });
}
Expand Down
Loading

0 comments on commit 36ba0a9

Please sign in to comment.