Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@devts/authjs

Package Overview
Dependencies
Maintainers
2
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@devts/authjs - npm Package Compare versions

Comparing version
0.0.7
to
0.0.8
+2
src/common/index.ts
export * from "./result.interface";
export * from "./is";
import { IResult, IOk, IError } from "./result.interface";
export const isOk = <T, E>(input: IResult<T, E>): input is IOk<T> =>
input.type === "ok";
export const isError = <T, E>(input: IResult<T, E>): input is IError<E> =>
input.type === "error";
export type IResult<T, E> = IOk<T> | IError<E>;
export interface IOk<T> {
readonly type: "ok";
readonly result: T;
}
export interface IError<E> {
readonly type: "error";
readonly result: E;
}
import * as authjs from "./module";
export * from "./module";
export default authjs;
import { JwtExtractorFromExpressRequest } from "./interface";
export namespace JwtExtractFrom {
export const Header =
(key: string): JwtExtractorFromExpressRequest =>
(req) => {
const value = req.headers[key.toLowerCase()];
if (typeof value !== "string") return { type: "error", result: null };
return { type: "ok", result: value };
};
export const Body =
(key: string): JwtExtractorFromExpressRequest =>
(req) => {
const body = req.body;
if (body == null) return { type: "error", result: null };
if (typeof body !== "object") return { type: "error", result: null };
const value = (body as any)[key] as unknown;
if (typeof value !== "string") return { type: "error", result: null };
return { type: "ok", result: value };
};
export const Query =
(key: string): JwtExtractorFromExpressRequest =>
(req) => {
const value = req.query[key];
if (typeof value !== "string") return { type: "error", result: null };
return { type: "ok", result: value };
};
export const Cookie =
(key: string): JwtExtractorFromExpressRequest =>
(req) => {
const cookies = req["cookies"] as unknown;
if (cookies == null) return { type: "error", result: null };
if (typeof cookies !== "object") return { type: "error", result: null };
const value = (cookies as any)[key] as unknown;
if (typeof value !== "string") return { type: "error", result: null };
return { type: "ok", result: value };
};
export const AuthorizationHeader =
(token_type: string): JwtExtractorFromExpressRequest =>
(req) => {
const regex = new RegExp(`^${token_type}\\s+\\S+`, "i");
const line = req.headers["authorization"];
if (line === undefined) return { type: "error", result: null };
const token = line.match(regex)?.[0].split(/\s+/)[1];
return typeof token === "undefined"
? { type: "error", result: null }
: { type: "ok", result: token };
};
export const AuthorizationHeaderAsBearer = AuthorizationHeader("bearer");
}
export * from "./extractor";
export * from "./interface";
import { IResult } from "../common/result.interface";
import Express from "express";
export type JwtExtractorFromExpressRequest = (
req: Express.Request<
Record<string, string>,
unknown,
unknown,
Record<string, string | string[] | undefined>,
Record<string, unknown>
>
) => IResult<string, null>;
export * from "./common";
export * as Jwt from "./jwt";
export * as Github from "./oauth/github";
export * as Kakao from "./oauth/kakao";
export * from "./interface";
export * from "./provider";
export interface IOauth2Options {
readonly client_id: string;
readonly client_secret: string;
readonly redirect_uri: string;
readonly scope: Scope[];
readonly allow_signup?: boolean;
}
export type Scope =
| "repo"
| "repo:status"
| "repo_deployment"
| "public_repo"
| "repo:invite"
| "security_events"
| "admin:repo_hook"
| "write:repo_hook"
| "read:repo_hook"
| "admin:org"
| "write:org"
| "read:org"
| "admin:public_key"
| "write:public_key"
| "read:public_key"
| "admin:org_hook"
| "gist"
| "notifications"
| "user"
| "read:user"
| "user:email"
| "user:follow"
| "project"
| "read:project"
| "delete_repo"
| "write:discussion"
| "read:discussion"
| "write:packages"
| "read:packages"
| "delete:packages"
| "admin:gpg_key"
| "write:gpg_key"
| "read:gpg_key"
| "codespace"
| "workflow";
/**
* If options's scope don't include 'user' or 'read:user', you will get PublicUser.
*/
export interface IPublicUser {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string | null;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
name: string | null;
company: string | null;
blog: string | null;
location: string | null;
email: string | null;
hireable: boolean | null;
bio: string | null;
twitter_username?: string | null;
public_repos: number;
public_gists: number;
followers: number;
following: number;
created_at: string;
updated_at: string;
plan?: {
collaborators: number;
name: string;
space: number;
private_repos: number;
[k: string]: unknown;
};
suspended_at?: string | null;
private_gists?: number;
total_private_repos?: number;
owned_private_repos?: number;
disk_usage?: number;
collaborators?: number;
}
/**
* If option's scope include 'user' or 'read:user', you will get PrivateUser.
*/
export interface IPrivateUser extends IPublicUser {
private_gists: number;
total_private_repos: number;
owned_private_repos: number;
disk_usage: number;
collaborators: number;
two_factor_authentication: boolean;
business_plus?: boolean;
ldap_dn?: string;
[k: string]: unknown;
}
export type IUser = IPublicUser | IPrivateUser;
export interface IEmail {
email: string;
primary: boolean;
verified: boolean;
visibility: "public" | "private" | null;
[k: string]: unknown;
}
export interface ITokens {
access_token: string;
token_type: string;
scope: string;
expires_in?: string;
refresh_token?: string;
refresh_token_expires_in?: string;
}
import { Fetcher } from "../../utils/fetcher";
import { IEmail, IOauth2Options, ITokens, IUser } from "./interface";
const LOGIN_URL = "https://github.com/login/oauth/authorize";
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
const API_BASE = "https://api.github.com";
export const getLoginUri = (options: IOauth2Options): string => {
const {
client_id,
client_secret,
redirect_uri,
scope,
allow_signup = true
} = options;
return (
LOGIN_URL +
"?" +
new URLSearchParams([
["client_id", client_id],
["client_secret", client_secret],
["redirect_uri", redirect_uri],
["allow_signup", allow_signup + ""],
["scope", scope.join(" ")]
]).toString()
);
};
export const getTokens = (options: IOauth2Options) => (code: string) =>
Fetcher.post<ITokens>({
uri: ACCESS_TOKEN_URL,
body: {
client_id: options.client_id,
client_secret: options.client_secret,
code
},
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
});
export const api =
<T>(path: string) =>
(access_token: string) =>
Fetcher.get<T>({
uri: API_BASE + path,
headers: {
Authorization: "Bearer " + access_token,
Accept: "application/vnd.github+json",
"X-Github-Api-Version": "2022-11-28"
}
});
export const getUser = api<IUser>("/user");
export const getEmails = api<IEmail[]>("/user/emails");
export * from "./interface";
export * from "./provider";
export interface IOauth2Options {
/**
* 앱 REST API 키
*
* [내 애플리케이션] > [앱 키]에서 확인 가능
*/
readonly client_id: string;
/**
* 토큰 발급 시, 보안을 강화하기 위해 추가 확인하는 코드
*
* [내 애플리케이션] > [보안]에서 설정 가능
*
* ON 상태인 경우 필수 설정해야 함
*/
readonly client_secret?: string;
/**
* 인가 코드를 전달받을 서비스 서버의 URI
*/
readonly redirect_uri: string;
/**
* 동의 화면 요청 시 추가 상호작용을 요청하고자 할 때 전달하는 파라미터
*
* 다음 값 사용 가능:
*
* login: 기존 사용자 인증 여부와 상관없이 사용자에게 카카오계정 로그인 화면을 출력하여 다시 사용자 인증을 수행하고자 할 때 사용, 카카오톡 인앱 브라우저에서는 이 기능이 제공되지 않음
*
* none: 사용자에게 동의 화면과 같은 대화형 UI를 노출하지 않고 인가 코드 발급을 요청할 때 사용, 인가 코드 발급을 위해 사용자의 동작이 필요한 경우 에러 응답 전달
*
* create: 사용자에게 카카오계정 신규 가입 후 로그인하도록 하기 위해 사용, 카카오계정 가입 페이지로 이동 후, 카카오계정 가입 완료 후 동의 화면 출력
*/
readonly prompt?: "login" | "none" | "create";
/**
* 약관 선택해 동의 받기 요청 시 사용
*
* 동의받을 약관 태그 목록
*
* 약관 태그는 [내 애플리케이션] > [간편가입]에서 확인 가능
*/
readonly service_terms: string[];
/**
* 카카오 로그인 과정 중 동일한 값을 유지하는 임의의 문자열(정해진 형식 없음)
*
* Cross-Site Request Forgery(CSRF) 공격으로부터 카카오 로그인 요청을 보호하기 위해 사용
*
* 각 사용자의 로그인 요청에 대한 state 값은 고유해야 함
*
* 인가 코드 요청, 인가 코드 응답, 토큰 발급 요청의 state 값 일치 여부로 요청 및 응답 유효성 확인 가능
*/
readonly state?: string;
/**
* OpenID Connect를 통해 ID 토큰을 함께 발급받을 경우, ID 토큰 재생 공격을 방지하기 위해 사용
*
* ID 토큰 유효성 검증 시 대조할 임의의 문자열(정해진 형식 없음)
*/
readonly nonce?: string;
}
export interface IMeRequestParameter {
/**
* 이미지 URL 값 HTTPS 여부, true 설정 시 HTTPS 사용, 기본 값 false
*/
readonly secure_resource?: boolean;
/**
* Property 키 목록, JSON Array를 ["kakao_account.email"]과 같은 형식으로 사용
*/
readonly property_keys: PropertyKey[];
}
export interface ITokens {
/**
* 토큰 타입, bearer로 고정
*/
readonly token_type: "bearer";
/**
* 사용자 액세스 토큰 값
*/
readonly access_token: string;
/**
* ID 토큰 값
*
* OpenID Connect 확장 기능을 통해 발급되는 ID 토큰, Base64 인코딩 된 사용자 인증 정보 포함
*
* 제공 조건: OpenID Connect가 활성화 된 앱의 토큰 발급 요청인 경우
*
* 또는 scope에 openid를 포함한 추가 항목 동의 받기 요청을 거친 토큰 발급 요청인 경우
*/
readonly id_token?: string;
/**
* 액세스 토큰과 ID 토큰의 만료 시간(초)
*
* 참고: 액세스 토큰과 ID 토큰의 만료 시간은 동일
*/
readonly expires_in: number;
/**
* 사용자 리프레시 토큰 값
*/
readonly refresh_token: string;
/**
* 리프레시 토큰 만료 시간(초)
*/
readonly refresh_token_expires_in: number;
/**
* 인증된 사용자의 정보 조회 권한 범위
*
* 범위가 여러 개일 경우, 공백으로 구분
*
* 참고: OpenID Connect가 활성화된 앱의 토큰 발급 요청인 경우, ID 토큰이 함께 발급되며 scope 값에 openid 포함
*/
readonly scope: string;
}
export interface IIdTokenPayload {
/**
* ID 토큰을 발급한 인증 기관 정보
*
* https://kauth.kakao.com로 고정
*/
readonly iss: "https://kauth.kakao.com";
/**
* ID 토큰이 발급된 앱의 앱 키
*
* 인가 코드 받기 요청 시 client_id에 전달된 앱 키
*
* Kakao SDK를 통한 카카오 로그인의 경우, 해당 SDK 초기화 시 사용된 앱 키
*/
readonly aud: string;
/**
* ID 토큰에 해당하는 사용자의 회원번호
*/
readonly sub: string;
/**
* ID 토큰 발급 또는 갱신 시각, UNIX 타임스탬프(Timestamp)
*/
readonly iat: number;
/**
* ID 토큰 만료 시간, UNIX 타임스탬프(Timestamp)
*/
readonly exp: number;
/**
* 사용자가 카카오 로그인을 통해 인증을 완료한 시각, UNIX 타임스탬프(Timestamp)
*/
readonly auth_time: number;
/**
* 인가 코드 받기 요청 시 전달한 nonce 값과 동일한 값
*
* ID 토큰 유효성 검증 시 사용
*/
readonly nonce?: string;
/**
* 닉네임
*
* 사용자 정보 가져오기의 kakao_account.profile.nickname에 해당
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 닉네임
*/
readonly nickname?: string;
/**
* 프로필 미리보기 이미지 URL
*
* 110px * 110px 또는 100px * 100px
*
* 사용자 정보 가져오기의 kakao_account.profile.thumbnail_image_url에 해당
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 프로필 사진
*/
readonly picture?: string;
/**
* 카카오계정 대표 이메일
*
* 사용자 정보 가져오기의 kakao_account.email에 해당
*
* 필요한 동의 항목: 카카오계정(이메일)
*
* 비고: ID 토큰 페이로드의 이메일은 유효하고 인증된 이메일 값이 있는 경우에만 제공, 이메일 사용 시 주의사항 참고
*/
readonly email?: string;
}
export type PropertyKey =
| "kakao_account.profile"
| "kakao_account.name"
| "kakao_account.email"
| "kakao_account.age_range"
| "kakao_account.birthday"
| "kakao_account.gender";
export interface IMeResponse {
/**
* 회원번호
*/
readonly id: number;
/**
* 자동 연결 설정을 비활성화한 경우만 존재
*
* 연결하기 호출의 완료 여부
* - false: 연결 대기(Preregistered) 상태
* - true: 연결(Registered) 상태
*/
readonly has_signed_up?: boolean;
/**
* 서비스에 연결 완료된 시각, UTC*
*
* yyyyMMddHHmmss 형식의 String
*/
readonly connected_at?: string;
/**
* 카카오싱크 간편가입을 통해 로그인한 시각, UTC*
*
* yyyyMMddHHmmss 형식의 String
*/
readonly synched_at?: string;
/**
* 사용자 프로퍼티(Property)
*
* 사용자 프로퍼티 참고
*/
readonly properties?: object;
/**
* 카카오계정 정보
*/
readonly kakao_account?: IKakaoAccount;
/**
* uuid 등 추가 정보
*/
readonly for_partner?: IPartner;
}
export interface IPartner {
/**
* 고유 ID
*
* 카카오톡 메시지 API 사용 권한이 있는 경우에만 제공
*
* 필요한 동의 항목: 카카오톡 메시지 전송(talk_message)
*/
readonly uuid?: string;
}
export interface IKakaoAccount {
/**
* 사용자 동의 시 프로필 정보(닉네임/프로필 사진) 제공 가능
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진)
*/
readonly profile_needs_agreement?: boolean;
/**
* 사용자 동의 시 닉네임 제공 가능
*
* 필요한 동의 항목: 닉네임
*/
readonly profile_nickname_needs_agreement?: boolean;
/**
* 사용자 동의 시 프로필 사진 제공 가능
*
* 필요한 동의 항목: 프로필 사진
*/
readonly profile_image_needs_agreement?: boolean;
/**
* 프로필 정보
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진), 닉네임, 프로필 사진
*/
readonly profile?: IProfile;
/**
* 사용자 동의 시 카카오계정 이름 제공 가능
*
* 필요한 동의 항목: 이름
*/
readonly name_needs_agreement?: boolean;
/**
* 카카오계정 이름
*
* 필요한 동의 항목: 이름
*/
readonly name?: string;
/**
* 사용자 동의 시 카카오계정 대표 이메일 제공 가능
*
* 필요한 동의 항목: 카카오계정(이메일)
*/
readonly email_needs_agreement?: boolean;
/**
* 이메일 유효 여부
* - true: 유효한 이메일
* - false: 이메일이 다른 카카오계정에 사용돼 만료
*
* 필요한 동의 항목: 카카오계정(이메일)
*/
readonly is_email_valid?: boolean;
/**
* 이메일 인증 여부
* - true: 인증된 이메일
* - false: 인증되지 않은 이메일
*
* 필요한 동의 항목: 카카오계정(이메일)
*/
readonly is_email_verified?: boolean;
/**
* 카카오계정 대표 이메일
*
* 필요한 동의 항목: 카카오계정(이메일)
*
* 비고: 이메일 사용 시 주의사항
*/
readonly email?: string;
/**
* 사용자 동의 시 연령대 제공 가능
*
* 필요한 동의 항목: 연령대
*/
readonly age_range_needs_agreement?: boolean;
/**
* 연령대
* - 1~9: 1세 이상 10세 미만
* - 10~14: 10세 이상 15세 미만
* - 15~19: 15세 이상 20세 미만
* - 20~29: 20세 이상 30세 미만
* - 30~39: 30세 이상 40세 미만
* - 40~49: 40세 이상 50세 미만
* - 50~59: 50세 이상 60세 미만
* - 60~69: 60세 이상 70세 미만
* - 70~79: 70세 이상 80세 미만
* - 80~89: 80세 이상 90세 미만
* - 90~: 90세 이상
*
* 필요한 동의 항목: 연령대
*/
readonly age_range?: string;
/**
* 사용자 동의 시 출생 연도 제공 가능
*
* 필요한 동의 항목: 출생 연도
*/
readonly birthyear_needs_agreement?: boolean;
/**
* 출생 연도(YYYY 형식)
*
* 필요한 동의 항목: 출생 연도
*/
readonly birthyear?: string;
/**
* 사용자 동의 시 생일 제공 가능
*
* 필요한 동의 항목: 생일
*/
readonly birthday_needs_agreement?: boolean;
/**
* 생일(MMDD 형식)
*
* 필요한 동의 항목: 생일
*/
readonly birthday?: string;
/**
* 생일 타입
* SOLAR(양력) 또는 LUNAR(음력)
*
* 필요한 동의 항목: 생일
*/
readonly birthday_type?: "SOLAR" | "LUNAR";
/**
* 사용자 동의 시 성별 제공 가능
*
* 필요한 동의 항목: 성별
*/
readonly gender_needs_agreement?: boolean;
/**
* 성별
* - female: 여성
* - male: 남성
*
* 필요한 동의 항목: 성별
*/
readonly gender?: "female" | "male";
/**
* 사용자 동의 시 전화번호 제공 가능
*
* 필요한 동의 항목: 카카오계정(전화번호)
*/
readonly phone_number_needs_agreement?: boolean;
/**
* 카카오계정의 전화번호
*
* 국내 번호인 경우 +82 00-0000-0000 형식
*
* 해외 번호인 경우 자릿수, 붙임표(-) 유무나 위치가 다를 수 있음
*
* (참고: libphonenumber)
*
* 필요한 동의 항목: 카카오계정(전화번호)
*/
readonly phone_number?: string;
/**
* 사용자 동의 시 CI 참고 가능
*
* 필요한 동의 항목: CI(연계정보)
*/
readonly ci_needs_agreement?: boolean;
/**
* 연계정보
*
* 필요한 동의 항목: CI(연계정보)
*/
readonly ci?: string;
/**
* CI 발급 시각, UTC*
*
* 필요한 동의 항목: CI(연계정보)
*/
readonly ci_authenticated_at?: string;
}
export interface IProfile {
/**
* 닉네임
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 닉네임
*/
readonly nickname?: string;
/**
* 프로필 미리보기 이미지 URL
*
* 110px * 110px 또는 100px * 100px
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 프로필 사진
*/
readonly thumbnail_image_url?: string;
/**
* 프로필 사진 URL
*
* 640px * 640px 또는 480px * 480px
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 프로필 사진
*/
readonly profile_image_url?: string;
/**
* 프로필 사진 URL이 기본 프로필 사진 URL인지 여부
* 사용자가 등록한 프로필 사진이 없을 경우, 기본 프로필 사진 제공
* true: 기본 프로필 사진
* false: 사용자가 등록한 프로필 사진
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 프로필 사진
*/
readonly is_default_image?: boolean;
}
import { Fetcher } from "../../utils/fetcher";
import {
IMeRequestParameter,
IMeResponse,
IOauth2Options,
ITokens
} from "./interface";
const LOGIN_URL = "https://kauth.kakao.com";
const API_URL = "https://kapi.kakao.com";
export const getLoginUri = (options: IOauth2Options) => {
const { client_id, redirect_uri, prompt, service_terms, state, nonce } =
options;
const path = "/oauth/authorize";
const search_params = new URLSearchParams({
client_id,
redirect_uri,
...(prompt ? { prompt } : {}),
...(service_terms.length > 0
? { service_terms: service_terms.join(",") }
: {}),
...(state ? { state } : {}),
...(nonce ? { nonce } : {}),
response_type: "code"
}).toString();
return LOGIN_URL + path + "?" + search_params;
};
export const getTokens =
({ client_id, client_secret, redirect_uri }: IOauth2Options) =>
(code: string) =>
Fetcher.post<ITokens>({
uri: LOGIN_URL + "/oauth/token",
body: new URLSearchParams({
grant_type: "authorization_code",
client_id,
redirect_uri,
code,
...(client_secret ? { client_secret } : {})
}).toString(),
headers: {
"Content-type": "application/x-www-form-urlencoded;charset=utf-8"
}
});
export const getMe =
({ secure_resource, property_keys }: IMeRequestParameter) =>
(access_token: string) =>
Fetcher.post<IMeResponse>({
uri: API_URL + "/v2/user/me",
body: new URLSearchParams({
grant_type: "authorization_code",
secure_resource: secure_resource ? "true" : "false",
property_keys: JSON.stringify(property_keys)
}).toString(),
headers: {
"Content-type": "application/x-www-form-urlencoded",
Authorization: `Bearer ${access_token}`
}
});
import { IResult } from "../common/result.interface";
export namespace Fetcher {
const _fetch = () => import("node-fetch").then(({ default: fetch }) => fetch);
const fetch = async (
url: URL | RequestInfo,
init?: RequestInit | undefined
) => (await _fetch())(url as any, init as any);
export const get = async <T>({
uri,
headers = {}
}: {
uri: string;
headers?: Record<string, string>;
}): Promise<IResult<T, string>> => {
try {
const response = await fetch(uri, {
method: "GET",
headers: {
"User-Agent": "request",
...headers
}
});
if (response.status !== 200 && response.status !== 201)
return { type: "error", result: await response.text() };
return {
type: "ok",
result: (await response.json()) as T
};
} catch (error: unknown) {
return {
type: "error",
result: error instanceof Error ? error.message : ""
};
}
};
export const post = async <T>({
uri,
body,
headers = {}
}: {
uri: string;
body: object | string;
headers?: Record<string, string>;
}): Promise<IResult<T, string>> => {
try {
const response = await fetch(uri, {
body: typeof body === "string" ? body : JSON.stringify(body),
headers: {
"User-Agent": "request",
...headers
}
});
if (response.status !== 200 && response.status !== 201)
return { type: "error", result: await response.text() };
return {
type: "ok",
result: (await response.json()) as T
};
} catch (error: unknown) {
return {
type: "error",
result: error instanceof Error ? error.message : ""
};
}
};
}
+1
-1
{
"name": "@devts/authjs",
"version": "0.0.7",
"version": "0.0.8",
"description": "Oauth2 Library",

@@ -5,0 +5,0 @@ "main": "lib/index.js",

export * from "./result.interface";
export * from "./is";
import { IResult, IOk, IError } from "./result.interface";
export const isOk = <T, E>(input: IResult<T, E>): input is IOk<T> =>
input.type === "ok";
export const isError = <T, E>(input: IResult<T, E>): input is IError<E> =>
input.type === "error";
export type IResult<T, E> = IOk<T> | IError<E>;
export interface IOk<T> {
readonly type: "ok";
readonly result: T;
}
export interface IError<E> {
readonly type: "error";
readonly result: E;
}
import * as authjs from "./module";
export * from "./module";
export default authjs;
import { JwtExtractorFromExpressRequest } from "./interface";
export namespace JwtExtractFrom {
export const Header =
(key: string): JwtExtractorFromExpressRequest =>
(req) => {
const value = req.headers[key.toLowerCase()];
if (typeof value !== "string") return { type: "error", result: null };
return { type: "ok", result: value };
};
export const Body =
(key: string): JwtExtractorFromExpressRequest =>
(req) => {
const body = req.body;
if (body == null) return { type: "error", result: null };
if (typeof body !== "object") return { type: "error", result: null };
const value = (body as any)[key] as unknown;
if (typeof value !== "string") return { type: "error", result: null };
return { type: "ok", result: value };
};
export const Query =
(key: string): JwtExtractorFromExpressRequest =>
(req) => {
const value = req.query[key];
if (typeof value !== "string") return { type: "error", result: null };
return { type: "ok", result: value };
};
export const Cookie =
(key: string): JwtExtractorFromExpressRequest =>
(req) => {
const cookies = req["cookies"] as unknown;
if (cookies == null) return { type: "error", result: null };
if (typeof cookies !== "object") return { type: "error", result: null };
const value = (cookies as any)[key] as unknown;
if (typeof value !== "string") return { type: "error", result: null };
return { type: "ok", result: value };
};
export const AuthorizationHeader =
(token_type: string): JwtExtractorFromExpressRequest =>
(req) => {
const regex = new RegExp(`^${token_type}\\s+\\S+`, "i");
const line = req.headers["authorization"];
if (line === undefined) return { type: "error", result: null };
const token = line.match(regex)?.[0].split(/\s+/)[1];
return typeof token === "undefined"
? { type: "error", result: null }
: { type: "ok", result: token };
};
export const AuthorizationHeaderAsBearer = AuthorizationHeader("bearer");
}
export * from "./extractor";
export * from "./interface";
import { IResult } from "../common/result.interface";
import Express from "express";
export type JwtExtractorFromExpressRequest = (
req: Express.Request<
Record<string, string>,
unknown,
unknown,
Record<string, string | string[] | undefined>,
Record<string, unknown>
>
) => IResult<string, null>;
export * from "./common";
export * as Jwt from "./jwt";
export * as Github from "./oauth/github";
export * as Kakao from "./oauth/kakao";
export * from "./interface";
export * from "./provider";
export interface IOauth2Options {
readonly client_id: string;
readonly client_secret: string;
readonly redirect_uri: string;
readonly scope: Scope[];
readonly allow_signup?: boolean;
}
export type Scope =
| "repo"
| "repo:status"
| "repo_deployment"
| "public_repo"
| "repo:invite"
| "security_events"
| "admin:repo_hook"
| "write:repo_hook"
| "read:repo_hook"
| "admin:org"
| "write:org"
| "read:org"
| "admin:public_key"
| "write:public_key"
| "read:public_key"
| "admin:org_hook"
| "gist"
| "notifications"
| "user"
| "read:user"
| "user:email"
| "user:follow"
| "project"
| "read:project"
| "delete_repo"
| "write:discussion"
| "read:discussion"
| "write:packages"
| "read:packages"
| "delete:packages"
| "admin:gpg_key"
| "write:gpg_key"
| "read:gpg_key"
| "codespace"
| "workflow";
/**
* If options's scope don't include 'user' or 'read:user', you will get PublicUser.
*/
export interface IPublicUser {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string | null;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
name: string | null;
company: string | null;
blog: string | null;
location: string | null;
email: string | null;
hireable: boolean | null;
bio: string | null;
twitter_username?: string | null;
public_repos: number;
public_gists: number;
followers: number;
following: number;
created_at: string;
updated_at: string;
plan?: {
collaborators: number;
name: string;
space: number;
private_repos: number;
[k: string]: unknown;
};
suspended_at?: string | null;
private_gists?: number;
total_private_repos?: number;
owned_private_repos?: number;
disk_usage?: number;
collaborators?: number;
}
/**
* If option's scope include 'user' or 'read:user', you will get PrivateUser.
*/
export interface IPrivateUser extends IPublicUser {
private_gists: number;
total_private_repos: number;
owned_private_repos: number;
disk_usage: number;
collaborators: number;
two_factor_authentication: boolean;
business_plus?: boolean;
ldap_dn?: string;
[k: string]: unknown;
}
export type IUser = IPublicUser | IPrivateUser;
export interface IEmail {
email: string;
primary: boolean;
verified: boolean;
visibility: "public" | "private" | null;
[k: string]: unknown;
}
export interface ITokens {
access_token: string;
token_type: string;
scope: string;
expires_in?: string;
refresh_token?: string;
refresh_token_expires_in?: string;
}
import { Fetcher } from "../../utils/fetcher";
import { IEmail, IOauth2Options, ITokens, IUser } from "./interface";
const LOGIN_URL = "https://github.com/login/oauth/authorize";
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
const API_BASE = "https://api.github.com";
export const getLoginUri = (options: IOauth2Options): string => {
const {
client_id,
client_secret,
redirect_uri,
scope,
allow_signup = true
} = options;
return (
LOGIN_URL +
"?" +
new URLSearchParams([
["client_id", client_id],
["client_secret", client_secret],
["redirect_uri", redirect_uri],
["allow_signup", allow_signup + ""],
["scope", scope.join(" ")]
]).toString()
);
};
export const getTokens = (options: IOauth2Options) => (code: string) =>
Fetcher.post<ITokens>({
uri: ACCESS_TOKEN_URL,
body: {
client_id: options.client_id,
client_secret: options.client_secret,
code
},
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
});
export const api =
<T>(path: string) =>
(access_token: string) =>
Fetcher.get<T>({
uri: API_BASE + path,
headers: {
Authorization: "Bearer " + access_token,
Accept: "application/vnd.github+json",
"X-Github-Api-Version": "2022-11-28"
}
});
export const getUser = api<IUser>("/user");
export const getEmails = api<IEmail[]>("/user/emails");
export * from "./interface";
export * from "./provider";
export interface IOauth2Options {
/**
* 앱 REST API 키
*
* [내 애플리케이션] > [앱 키]에서 확인 가능
*/
readonly client_id: string;
/**
* 토큰 발급 시, 보안을 강화하기 위해 추가 확인하는 코드
*
* [내 애플리케이션] > [보안]에서 설정 가능
*
* ON 상태인 경우 필수 설정해야 함
*/
readonly client_secret?: string;
/**
* 인가 코드를 전달받을 서비스 서버의 URI
*/
readonly redirect_uri: string;
/**
* 동의 화면 요청 시 추가 상호작용을 요청하고자 할 때 전달하는 파라미터
*
* 다음 값 사용 가능:
*
* login: 기존 사용자 인증 여부와 상관없이 사용자에게 카카오계정 로그인 화면을 출력하여 다시 사용자 인증을 수행하고자 할 때 사용, 카카오톡 인앱 브라우저에서는 이 기능이 제공되지 않음
*
* none: 사용자에게 동의 화면과 같은 대화형 UI를 노출하지 않고 인가 코드 발급을 요청할 때 사용, 인가 코드 발급을 위해 사용자의 동작이 필요한 경우 에러 응답 전달
*
* create: 사용자에게 카카오계정 신규 가입 후 로그인하도록 하기 위해 사용, 카카오계정 가입 페이지로 이동 후, 카카오계정 가입 완료 후 동의 화면 출력
*/
readonly prompt?: "login" | "none" | "create";
/**
* 약관 선택해 동의 받기 요청 시 사용
*
* 동의받을 약관 태그 목록
*
* 약관 태그는 [내 애플리케이션] > [간편가입]에서 확인 가능
*/
readonly service_terms: string[];
/**
* 카카오 로그인 과정 중 동일한 값을 유지하는 임의의 문자열(정해진 형식 없음)
*
* Cross-Site Request Forgery(CSRF) 공격으로부터 카카오 로그인 요청을 보호하기 위해 사용
*
* 각 사용자의 로그인 요청에 대한 state 값은 고유해야 함
*
* 인가 코드 요청, 인가 코드 응답, 토큰 발급 요청의 state 값 일치 여부로 요청 및 응답 유효성 확인 가능
*/
readonly state?: string;
/**
* OpenID Connect를 통해 ID 토큰을 함께 발급받을 경우, ID 토큰 재생 공격을 방지하기 위해 사용
*
* ID 토큰 유효성 검증 시 대조할 임의의 문자열(정해진 형식 없음)
*/
readonly nonce?: string;
}
export interface IMeRequestParameter {
/**
* 이미지 URL 값 HTTPS 여부, true 설정 시 HTTPS 사용, 기본 값 false
*/
readonly secure_resource?: boolean;
/**
* Property 키 목록, JSON Array를 ["kakao_account.email"]과 같은 형식으로 사용
*/
readonly property_keys: PropertyKey[];
}
export interface ITokens {
/**
* 토큰 타입, bearer로 고정
*/
readonly token_type: "bearer";
/**
* 사용자 액세스 토큰 값
*/
readonly access_token: string;
/**
* ID 토큰 값
*
* OpenID Connect 확장 기능을 통해 발급되는 ID 토큰, Base64 인코딩 된 사용자 인증 정보 포함
*
* 제공 조건: OpenID Connect가 활성화 된 앱의 토큰 발급 요청인 경우
*
* 또는 scope에 openid를 포함한 추가 항목 동의 받기 요청을 거친 토큰 발급 요청인 경우
*/
readonly id_token?: string;
/**
* 액세스 토큰과 ID 토큰의 만료 시간(초)
*
* 참고: 액세스 토큰과 ID 토큰의 만료 시간은 동일
*/
readonly expires_in: number;
/**
* 사용자 리프레시 토큰 값
*/
readonly refresh_token: string;
/**
* 리프레시 토큰 만료 시간(초)
*/
readonly refresh_token_expires_in: number;
/**
* 인증된 사용자의 정보 조회 권한 범위
*
* 범위가 여러 개일 경우, 공백으로 구분
*
* 참고: OpenID Connect가 활성화된 앱의 토큰 발급 요청인 경우, ID 토큰이 함께 발급되며 scope 값에 openid 포함
*/
readonly scope: string;
}
export interface IIdTokenPayload {
/**
* ID 토큰을 발급한 인증 기관 정보
*
* https://kauth.kakao.com로 고정
*/
readonly iss: "https://kauth.kakao.com";
/**
* ID 토큰이 발급된 앱의 앱 키
*
* 인가 코드 받기 요청 시 client_id에 전달된 앱 키
*
* Kakao SDK를 통한 카카오 로그인의 경우, 해당 SDK 초기화 시 사용된 앱 키
*/
readonly aud: string;
/**
* ID 토큰에 해당하는 사용자의 회원번호
*/
readonly sub: string;
/**
* ID 토큰 발급 또는 갱신 시각, UNIX 타임스탬프(Timestamp)
*/
readonly iat: number;
/**
* ID 토큰 만료 시간, UNIX 타임스탬프(Timestamp)
*/
readonly exp: number;
/**
* 사용자가 카카오 로그인을 통해 인증을 완료한 시각, UNIX 타임스탬프(Timestamp)
*/
readonly auth_time: number;
/**
* 인가 코드 받기 요청 시 전달한 nonce 값과 동일한 값
*
* ID 토큰 유효성 검증 시 사용
*/
readonly nonce?: string;
/**
* 닉네임
*
* 사용자 정보 가져오기의 kakao_account.profile.nickname에 해당
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 닉네임
*/
readonly nickname?: string;
/**
* 프로필 미리보기 이미지 URL
*
* 110px * 110px 또는 100px * 100px
*
* 사용자 정보 가져오기의 kakao_account.profile.thumbnail_image_url에 해당
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 프로필 사진
*/
readonly picture?: string;
/**
* 카카오계정 대표 이메일
*
* 사용자 정보 가져오기의 kakao_account.email에 해당
*
* 필요한 동의 항목: 카카오계정(이메일)
*
* 비고: ID 토큰 페이로드의 이메일은 유효하고 인증된 이메일 값이 있는 경우에만 제공, 이메일 사용 시 주의사항 참고
*/
readonly email?: string;
}
export type PropertyKey =
| "kakao_account.profile"
| "kakao_account.name"
| "kakao_account.email"
| "kakao_account.age_range"
| "kakao_account.birthday"
| "kakao_account.gender";
export interface IMeResponse {
/**
* 회원번호
*/
readonly id: number;
/**
* 자동 연결 설정을 비활성화한 경우만 존재
*
* 연결하기 호출의 완료 여부
* - false: 연결 대기(Preregistered) 상태
* - true: 연결(Registered) 상태
*/
readonly has_signed_up?: boolean;
/**
* 서비스에 연결 완료된 시각, UTC*
*
* yyyyMMddHHmmss 형식의 String
*/
readonly connected_at?: string;
/**
* 카카오싱크 간편가입을 통해 로그인한 시각, UTC*
*
* yyyyMMddHHmmss 형식의 String
*/
readonly synched_at?: string;
/**
* 사용자 프로퍼티(Property)
*
* 사용자 프로퍼티 참고
*/
readonly properties?: object;
/**
* 카카오계정 정보
*/
readonly kakao_account?: IKakaoAccount;
/**
* uuid 등 추가 정보
*/
readonly for_partner?: IPartner;
}
export interface IPartner {
/**
* 고유 ID
*
* 카카오톡 메시지 API 사용 권한이 있는 경우에만 제공
*
* 필요한 동의 항목: 카카오톡 메시지 전송(talk_message)
*/
readonly uuid?: string;
}
export interface IKakaoAccount {
/**
* 사용자 동의 시 프로필 정보(닉네임/프로필 사진) 제공 가능
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진)
*/
readonly profile_needs_agreement?: boolean;
/**
* 사용자 동의 시 닉네임 제공 가능
*
* 필요한 동의 항목: 닉네임
*/
readonly profile_nickname_needs_agreement?: boolean;
/**
* 사용자 동의 시 프로필 사진 제공 가능
*
* 필요한 동의 항목: 프로필 사진
*/
readonly profile_image_needs_agreement?: boolean;
/**
* 프로필 정보
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진), 닉네임, 프로필 사진
*/
readonly profile?: IProfile;
/**
* 사용자 동의 시 카카오계정 이름 제공 가능
*
* 필요한 동의 항목: 이름
*/
readonly name_needs_agreement?: boolean;
/**
* 카카오계정 이름
*
* 필요한 동의 항목: 이름
*/
readonly name?: string;
/**
* 사용자 동의 시 카카오계정 대표 이메일 제공 가능
*
* 필요한 동의 항목: 카카오계정(이메일)
*/
readonly email_needs_agreement?: boolean;
/**
* 이메일 유효 여부
* - true: 유효한 이메일
* - false: 이메일이 다른 카카오계정에 사용돼 만료
*
* 필요한 동의 항목: 카카오계정(이메일)
*/
readonly is_email_valid?: boolean;
/**
* 이메일 인증 여부
* - true: 인증된 이메일
* - false: 인증되지 않은 이메일
*
* 필요한 동의 항목: 카카오계정(이메일)
*/
readonly is_email_verified?: boolean;
/**
* 카카오계정 대표 이메일
*
* 필요한 동의 항목: 카카오계정(이메일)
*
* 비고: 이메일 사용 시 주의사항
*/
readonly email?: string;
/**
* 사용자 동의 시 연령대 제공 가능
*
* 필요한 동의 항목: 연령대
*/
readonly age_range_needs_agreement?: boolean;
/**
* 연령대
* - 1~9: 1세 이상 10세 미만
* - 10~14: 10세 이상 15세 미만
* - 15~19: 15세 이상 20세 미만
* - 20~29: 20세 이상 30세 미만
* - 30~39: 30세 이상 40세 미만
* - 40~49: 40세 이상 50세 미만
* - 50~59: 50세 이상 60세 미만
* - 60~69: 60세 이상 70세 미만
* - 70~79: 70세 이상 80세 미만
* - 80~89: 80세 이상 90세 미만
* - 90~: 90세 이상
*
* 필요한 동의 항목: 연령대
*/
readonly age_range?: string;
/**
* 사용자 동의 시 출생 연도 제공 가능
*
* 필요한 동의 항목: 출생 연도
*/
readonly birthyear_needs_agreement?: boolean;
/**
* 출생 연도(YYYY 형식)
*
* 필요한 동의 항목: 출생 연도
*/
readonly birthyear?: string;
/**
* 사용자 동의 시 생일 제공 가능
*
* 필요한 동의 항목: 생일
*/
readonly birthday_needs_agreement?: boolean;
/**
* 생일(MMDD 형식)
*
* 필요한 동의 항목: 생일
*/
readonly birthday?: string;
/**
* 생일 타입
* SOLAR(양력) 또는 LUNAR(음력)
*
* 필요한 동의 항목: 생일
*/
readonly birthday_type?: "SOLAR" | "LUNAR";
/**
* 사용자 동의 시 성별 제공 가능
*
* 필요한 동의 항목: 성별
*/
readonly gender_needs_agreement?: boolean;
/**
* 성별
* - female: 여성
* - male: 남성
*
* 필요한 동의 항목: 성별
*/
readonly gender?: "female" | "male";
/**
* 사용자 동의 시 전화번호 제공 가능
*
* 필요한 동의 항목: 카카오계정(전화번호)
*/
readonly phone_number_needs_agreement?: boolean;
/**
* 카카오계정의 전화번호
*
* 국내 번호인 경우 +82 00-0000-0000 형식
*
* 해외 번호인 경우 자릿수, 붙임표(-) 유무나 위치가 다를 수 있음
*
* (참고: libphonenumber)
*
* 필요한 동의 항목: 카카오계정(전화번호)
*/
readonly phone_number?: string;
/**
* 사용자 동의 시 CI 참고 가능
*
* 필요한 동의 항목: CI(연계정보)
*/
readonly ci_needs_agreement?: boolean;
/**
* 연계정보
*
* 필요한 동의 항목: CI(연계정보)
*/
readonly ci?: string;
/**
* CI 발급 시각, UTC*
*
* 필요한 동의 항목: CI(연계정보)
*/
readonly ci_authenticated_at?: string;
}
export interface IProfile {
/**
* 닉네임
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 닉네임
*/
readonly nickname?: string;
/**
* 프로필 미리보기 이미지 URL
*
* 110px * 110px 또는 100px * 100px
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 프로필 사진
*/
readonly thumbnail_image_url?: string;
/**
* 프로필 사진 URL
*
* 640px * 640px 또는 480px * 480px
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 프로필 사진
*/
readonly profile_image_url?: string;
/**
* 프로필 사진 URL이 기본 프로필 사진 URL인지 여부
* 사용자가 등록한 프로필 사진이 없을 경우, 기본 프로필 사진 제공
* true: 기본 프로필 사진
* false: 사용자가 등록한 프로필 사진
*
* 필요한 동의 항목: 프로필 정보(닉네임/프로필 사진) 또는 프로필 사진
*/
readonly is_default_image?: boolean;
}
import { Fetcher } from "../../utils/fetcher";
import {
IMeRequestParameter,
IMeResponse,
IOauth2Options,
ITokens
} from "./interface";
const LOGIN_URL = "https://kauth.kakao.com";
const API_URL = "https://kapi.kakao.com";
export const getLoginUri = (options: IOauth2Options) => {
const { client_id, redirect_uri, prompt, service_terms, state, nonce } =
options;
const path = "/oauth/authorize";
const search_params = new URLSearchParams({
client_id,
redirect_uri,
...(prompt ? { prompt } : {}),
...(service_terms.length > 0
? { service_terms: service_terms.join(",") }
: {}),
...(state ? { state } : {}),
...(nonce ? { nonce } : {}),
response_type: "code"
}).toString();
return LOGIN_URL + path + "?" + search_params;
};
export const getTokens =
({ client_id, client_secret, redirect_uri }: IOauth2Options) =>
(code: string) =>
Fetcher.post<ITokens>({
uri: LOGIN_URL + "/oauth/token",
body: new URLSearchParams({
grant_type: "authorization_code",
client_id,
redirect_uri,
code,
...(client_secret ? { client_secret } : {})
}).toString(),
headers: {
"Content-type": "application/x-www-form-urlencoded;charset=utf-8"
}
});
export const getMe =
({ secure_resource, property_keys }: IMeRequestParameter) =>
(access_token: string) =>
Fetcher.post<IMeResponse>({
uri: API_URL + "/v2/user/me",
body: new URLSearchParams({
grant_type: "authorization_code",
secure_resource: secure_resource ? "true" : "false",
property_keys: JSON.stringify(property_keys)
}).toString(),
headers: {
"Content-type": "application/x-www-form-urlencoded",
Authorization: `Bearer ${access_token}`
}
});
import { IResult } from "../common/result.interface";
export namespace Fetcher {
const _fetch = () => import("node-fetch").then(({ default: fetch }) => fetch);
const fetch = async (
url: URL | RequestInfo,
init?: RequestInit | undefined
) => (await _fetch())(url as any, init as any);
export const get = async <T>({
uri,
headers = {}
}: {
uri: string;
headers?: Record<string, string>;
}): Promise<IResult<T, string>> => {
try {
const response = await fetch(uri, {
method: "GET",
headers: {
"User-Agent": "request",
...headers
}
});
if (response.status !== 200 && response.status !== 201)
return { type: "error", result: await response.text() };
return {
type: "ok",
result: (await response.json()) as T
};
} catch (error: unknown) {
return {
type: "error",
result: error instanceof Error ? error.message : ""
};
}
};
export const post = async <T>({
uri,
body,
headers = {}
}: {
uri: string;
body: object | string;
headers?: Record<string, string>;
}): Promise<IResult<T, string>> => {
try {
const response = await fetch(uri, {
body: typeof body === "string" ? body : JSON.stringify(body),
headers: {
"User-Agent": "request",
...headers
}
});
if (response.status !== 200 && response.status !== 201)
return { type: "error", result: await response.text() };
return {
type: "ok",
result: (await response.json()) as T
};
} catch (error: unknown) {
return {
type: "error",
result: error instanceof Error ? error.message : ""
};
}
};
}