보안 이벤트 구독 관련 질문 드립니다

현재 node+express 환경에서 카카오 로그인 기능을 지원하고 있습니다.
보안 이벤트 구독 관련하여 기능 테스트중에 token paring에 관련하여 문의 드립니다.

node express 환경에서 application/secevent+jwt 타입을 parsing 하려면
필요한것들이 있을까요?

안녕하세요.

간단히 토큰 파싱해서 보시려면 아래 사이트 사용하시면되구요.

https://jwt.io/

각종 언어별로 라이브러리 제공하는 것 같은데 확인해보시겠어요?

https://jwt.io/libraries

토큰이 body에 담겨져오는게 아닌가요? req나 req.body 어디에도 토큰값이 전달이 안되서요

보안 이벤트 구독에 보면

  1. 이벤트 알림 받을 콜백 URL 등록
  2. 카테고리 선택 (Oauth 선택)
  3. 이벤트 타입 (사용자 카카오 로그인 해제 상황 테스트중 - userUnlink)
  4. parameter - reason UNLINK_FROM_APPS

발송하면 개발자 페이지에서는 발송한 토큰에 대해 확인이 가능한데
콜백 받은 주소로 들어온 로그를 보면 토큰값이 없어서 문의 드린겁니다

확인을 위해 앱 ID 부탁드립니다.


앱ID
https://developers.kakao.com/ 의 내 애플리케이션>앱 설정>요약 정보 : 기본정보에 있는 앱 ID
숫자로된 ID 입니다
ex) 123456

505902 입니다

로그를 보니 4건중 3건 이 타임아웃 응답을 받았네요. 값은 잘 전달되었습니다.
(unlink 관련된 것은 2건)

그럼 서버에서 파싱하는 방법이 잘못된걸까요?
서버에서는 무조건 202 응답하고 콘솔로그만 찍어놨는데…

4건중 카카오가 202응답받은건 1건입니다.

방금 보안 이벤트 구독 unlink 를 다른 환경(PHP)에서 해보니 테스트/실제 모두 다 잘 응답하네요.

그럼 토큰값은 body 에 담겨져 온다는 말씀이시죠?

네~ body에 들어옵니다~

(response 전체 로그 확인하셔서 개발하시는 환경 어느 영역에 세팅되는지 확인해보시면 좋을 것 같습니다.)

현재 서버에서 response 전체 로그와 body 둘다 콘솔로 찍어두고 확인하고 있고
express.json 혹은 bodypaser.json 과 같이 secevent+jwt 타입도 파싱 옵션을 따로 주어야하는건지 문의를 드렸던건데

답변 주신거는 express 에서
따로 secevent+jwt content type에 대한 파싱 옵션 없이도 확인할 수 있다는 말씀이신거죠??

response 전체 로그 기재해주시겠어요?

PHP에서는 그냥 raw body 가 토큰이라 별다른 조치 할 것없이
해당 토큰 파싱만 하면 되네요.

-- kakao svr noti req-- <ref *2> IncomingMessage {

_readableState: ReadableState {

objectMode: false,

highWaterMark: 16384,

buffer: BufferList { head: null, tail: null, length: 0 },

length: 0,

pipes: [],

flowing: null,

ended: false,

endEmitted: false,

reading: false,

sync: true,

needReadable: false,

emittedReadable: false,

readableListening: false,

resumeScheduled: false,

errorEmitted: false,

emitClose: true,

autoDestroy: false,

destroyed: false,

errored: null,

closed: false,

closeEmitted: false,

defaultEncoding: 'utf8',

awaitDrainWriters: null,

multiAwaitDrain: false,

readingMore: true,

dataEmitted: false,

decoder: null,

encoding: null,

[Symbol(kPaused)]: null

},

_events: [Object: null prototype] { end: [Function: clearRequestTimeout] },

_eventsCount: 1,

_maxListeners: undefined,

socket: <ref *1> Socket {

connecting: false,

_hadError: false,

_parent: null,

_host: null,

_readableState: ReadableState {

objectMode: false,

highWaterMark: 16384,

buffer: BufferList { head: null, tail: null, length: 0 },

length: 0,

pipes: [],

flowing: true,

ended: false,

endEmitted: false,

reading: true,

sync: false,

needReadable: true,

emittedReadable: false,

readableListening: false,

resumeScheduled: false,

errorEmitted: false,

emitClose: false,

autoDestroy: false,

destroyed: false,

errored: null,

closed: false,

closeEmitted: false,

defaultEncoding: 'utf8',

awaitDrainWriters: null,

multiAwaitDrain: false,

readingMore: false,

dataEmitted: false,

decoder: null,

encoding: null,

[Symbol(kPaused)]: false

},

_events: [Object: null prototype] {

end: [Array],

timeout: [Function: socketOnTimeout],

data: [Function: bound socketOnData],

error: [Function: socketOnError],

close: [Array],

drain: [Function: bound socketOnDrain],

resume: [Function: onSocketResume],

pause: [Function: onSocketPause]

},

_eventsCount: 8,

_maxListeners: undefined,

_writableState: WritableState {

objectMode: false,

highWaterMark: 16384,

finalCalled: false,

needDrain: false,

ending: false,

ended: false,

finished: false,

destroyed: false,

decodeStrings: false,

defaultEncoding: 'utf8',

length: 0,

writing: false,

corked: 0,

sync: true,

bufferProcessing: false,

onwrite: [Function: bound onwrite],

writecb: null,

writelen: 0,

afterWriteTickInfo: null,

buffered: [],

bufferedIndex: 0,

allBuffers: true,

allNoop: true,

pendingcb: 0,

prefinished: false,

errorEmitted: false,

emitClose: false,

autoDestroy: false,

errored: null,

closed: false,

closeEmitted: false

},allowHalfOpen: true

_sockname: null,

_pendingData: null,

_pendingEncoding: '',

server: Server {

maxHeaderSize: undefined,

insecureHTTPParser: undefined,

_events: [Object: null prototype],

_eventsCount: 4,

_maxListeners: undefined,

_connections: 1,

_handle: [TCP],

_usingWorkers: false,

_workers: [],

_unref: false,

allowHalfOpen: true,

pauseOnConnect: false,

httpAllowHalfOpen: false,

timeout: 0,

keepAliveTimeout: 5000,

maxHeadersCount: null,

headersTimeout: 60000,

requestTimeout: 0,

_connectionKey: '6::::8080',

[Symbol(IncomingMessage)]: [Function: IncomingMessage],

[Symbol(ServerResponse)]: [Function: ServerResponse],

[Symbol(kCapture)]: false,

[Symbol(async_id_symbol)]: 6

},

_server: Server {

maxHeaderSize: undefined,

insecureHTTPParser: undefined,

_events: [Object: null prototype],

_eventsCount: 4,

_maxListeners: undefined,

_connections: 1,

_handle: [TCP],

_usingWorkers: false,

_workers: [],

_unref: false,

allowHalfOpen: true,

pauseOnConnect: false,

httpAllowHalfOpen: false,

timeout: 0,

keepAliveTimeout: 5000,

maxHeadersCount: null,

headersTimeout: 60000,

requestTimeout: 0,

_connectionKey: '6::::8080',

[Symbol(IncomingMessage)]: [Function: IncomingMessage],

[Symbol(ServerResponse)]: [Function: ServerResponse],

[Symbol(kCapture)]: false,

[Symbol(async_id_symbol)]: 6

},

parser: HTTPParser {

'0': [Function: bound setRequestTimeout],

'1': [Function: parserOnHeaders],

'2': [Function: parserOnHeadersComplete],

'3': [Function: parserOnBody],

'4': [Function: parserOnMessageComplete],

'5': [Function: bound onParserExecute],

'6': [Function: bound onParserTimeout],

_headers: [],

_url: '',

socket: [Circular *1],

incoming: [Circular *2],

outgoing: null,

maxHeaderPairs: 2000,

_consumed: true,

onIncoming: [Function: bound parserOnIncoming],

Symbol(resource_symbol)]: [HTTPServerAsyncResource]

},

on: [Function: socketListenerWrap],

addListener: [Function: socketListenerWrap],

prependListener: [Function: socketListenerWrap],

_paused: false,

_httpMessage: ServerResponse {

_events: [Object: null prototype],

_eventsCount: 1,

_maxListeners: undefined,

outputData: [],

outputSize: 0,

writable: true,

destroyed: false,

_last: false,

chunkedEncoding: false,

shouldKeepAlive: true,

_defaultKeepAlive: true,

useChunkedEncodingByDefault: true,

sendDate: true,

_removedConnection: false,

_removedContLen: false,

_removedTE: false,

_contentLength: null,

_hasBody: true,

_trailer: '',

finished: false,

_headerSent: false,

socket: [Circular *1],

_header: null,

_keepAliveTimeout: 5000,

_onPendingData: [Function: bound updateOutgoingData],

_sent100: false,

_expect_continue: false,

req: [Circular *2],

locals: [Object: null prototype] {},

end: [Function (anonymous)],

[Symbol(kCapture)]: false,

[Symbol(kNeedDrain)]: false,

[Symbol(corked)]: 0,

[Symbol(kOutHeaders)]: [Object: null prototype]

},

[Symbol(async_id_symbol)]: 3163,

[Symbol(kHandle)]: TCP {

reading: true,

onconnection: null,

_consumed: true,

[Symbol(owner_symbol)]: [Circular *1]

},

[Symbol(kSetNoDelay)]: false,

[Symbol(lastWriteQueueSize)]: 0,

[Symbol(timeout)]: null,

[Symbol(kBuffer)]: null,

[Symbol(kBufferCb)]: null,

[Symbol(kBufferGen)]: null,

[Symbol(kCapture)]: false,

[Symbol(kBytesRead)]: 00

[Symbol(kBytesWritten)]: 0,

[Symbol(RequestTimeout)]: undefined

},

httpVersionMajor: 1,

httpVersionMinor: 1,

httpVersion: '1.1',

complete: false,

headers: {

connection: 'upgrade',

host: 'api-staging.thegoldclass.net',

'x-real-ip': '172.40.0.177',

'x-forwarded-for': '220.64.111.158, 172.40.0.177',

'content-length': '958',

'x-forwarded-proto': 'https',

'x-forwarded-port': '443',

'x-amzn-trace-id': 'Root=1-64bdd6bd-428e9eef152c905b0dc01c8f',

accept: 'application/json',

'content-type': 'application/secevent+jwt''user-agent': 'Kakao-HttpClient/2.3.3'

},

rawHeaders: [

'Connection',

'upgrade',

'Host',

'api-staging.thegoldclass.net',

'X-Real-IP',

'172.40.0.177',

'X-Forwarded-For',

'220.64.111.158, 172.40.0.177',

'Content-Length',

'958',

'X-Forwarded-Proto',

'https',

'X-Forwarded-Port',

'443',

'X-Amzn-Trace-Id',

'Root=1-64bdd6bd-428e9eef152c905b0dc01c8f',

'Accept',

'application/json',

'Content-Type',

'application/secevent+jwt',

'user-agent',

'Kakao-HttpClient/2.3.3'

],

trailers: {},

rawTrailers: [],

aborted: false,

upgrade: false,

url: '/kakaoSvrNoti',

method: 'POST',

statusCode: null,

statusMessage: null,

client: <ref *1> Socket {

connecting: false,

_hadError: false,

_parent: null,

_host: null,

_readableState: ReadableState {

objectMode: false,

highWaterMark: 16384,

buffer: BufferList { head: null, tail: null, length: 0 },

length: 0,

pipes: [],

flowing: true,

ended: false,

endEmitted: false,

reading: true,

sync: false,

needReadable: true,

emittedReadable: false,

readableListening: false,

resumeScheduled: false,

errorEmitted: false,

emitClose: false,

autoDestroy: false,

destroyed: false,

errored: null,

closed: false,

closeEmitted: false,

defaultEncoding: 'utf8',

awaitDrainWriters: null,

multiAwaitDrain: false,

readingMore: false,

dataEmitted: false,

decoder: null,

encoding: null,

[Symbol(kPaused)]: false

},

_events: [Object: null prototype] {

end: [Array],

timeout: [Function: socketOnTimeout],

data: [Function: bound socketOnData],

error: [Function: socketOnError],

close: [Array],

drain: [Function: bound socketOnDrain],

resume: [Function: onSocketResume],

pause: [Function: onSocketPause]

},

_eventsCount: 8,

_maxListeners: undefined,

_writableState: WritableState {

objectMode: false,

highWaterMark: 16384,

finalCalled: false,

needDrain: false,

ending: false,

ended: false,

finished: false,

destroyed: false,

decodeStrings: false,

defaultEncoding: 'utf8',

length: 0,

writing: false,

: corked: 0,

sync: true,

bufferProcessing: false,

onwrite: [Function: bound onwrite],

writecb: null,

writelen: 0,

afterWriteTickInfo: null,

buffered: [],

bufferedIndex: 0,

allBuffers: true,

allNoop: true,

pendingcb: 0,

prefinished: false,

errorEmitted: false,

emitClose: false,

autoDestroy: false,

errored: null,

closed: false,

closeEmitted: false

},

allowHalfOpen: true,

_sockname: null,

_pendingData: null,

_pendingEncoding: '',

Server {

maxHeaderSize: undefined,

insecureHTTPParser: undefined,

_events: [Object: null prototype],

_eventsCount: 4,

_maxListeners: undefined,

_connections: 1,

_handle: [TCP],

_usingWorkers: false,

_workers: [],

_unref: false,

allowHalfOpen: true,

pauseOnConnect: false,

httpAllowHalfOpen: false,

timeout: 0,

keepAliveTimeout: 5000

maxHeadersCount: null,

headersTimeout: 60000,

requestTimeout: 0,

_connectionKey: '6::::8080',

[Symbol(IncomingMessage)]: [Function: IncomingMessage],

[Symbol(ServerResponse)]: [Function: ServerResponse],

[Symbol(kCapture)]: false,

[Symbol(async_id_symbol)]: 6

},

_server: Server {

maxHeaderSize: undefined,

insecureHTTPParser: undefined,

_events: [Object: null prototype],

_eventsCount: 4,

_maxListeners: undefined,

_connections: 1,

_handle: [TCP],

_usingWorkers: false,

_workers: [],

_unref: false,

allowHalfOpen: true,

pauseOnConnect: false,

httpAllowHalfOpen: false,

timeout: 0,

keepAliveTimeout: 5000,

maxHeadersount: null,

headersTimeout: 60000,

requestTimeout: 0,

_connectionKey: '6::::8080',

[Symbol(IncomingMessage)]: [Function: IncomingMessage],

[Symbol(ServerResponse)]: [Function: ServerResponse],

[Symbol(kCapture)]: false,[

[Symbol(async_id_symbol)]: 6

[ },

[ parser: HTTPParser {

['0': [Function: bound setRquestTimeout],

['1': [Function: parserOnHeaders],

['2': [Function: parserOnHeadersComplete],

['3': [Function: parserOnBody],

['4': [Function: parserOnMessageComplete],

['5': [Function: bound onParserExecute],

['6': [Function: bound onParserTimeout],

[_headers: [],

[_url: '',

[socket: [Circular *1],

[incoming: [Circular *2],

[outgoing: null,

[ maxHeaderPairs: 2000,

[ _consumed: true,

[onIncoming: [Function: bound parserOnIncoming],

[ [Symbol(resource_symbol)]: [HTTPServerAsyncResource]

[ },

[ on: [Function: socketListenerWrap],

[ addListener: [Function: socketListenerWrap],

[ prependListener: [Function: socketListenerWrap],

[ _paused: false,

[ _httpMessage: ServerResponse {

[ _events: [Object: null prototype],

_eventsCount: 1,

_maxListeners: undefined,

outputData: [],

outputSize: 0,

writable: true,

destroyed: false,

_last: false,

chunkedEncoding: false,

shouldKeepAlive: true,

_defaultKeepAlive: true,

useChunkedEncodingByDefault: true,

sendDate: true,

_removedConnection: false,

_removedContLen: false,

_removedTE: false,

_contentLength: null,

_hasBody: true,

_trailer: '',

finished: false,

_headerSent: false,

socket: [Circular *1],

_header: null,

_keepAliveTimeout: 5000,

_onPendingData: [Function: bound updateOutgoingData],

_sent100: false,

_expect_continue: false,

req: [Circular *2],

locals: [Object: null prototype] {},

end: [Function (anonymous)],

[Symbol(kCapture)]: false,

[Symbol(kNeedDrain)]: false,

[Symbol(corked)]: 0,

[Symbol(kOutHeaders): [Object: null prototype]

},

[Symbol(async_id_symbol)]: 3163,

[Symbol(kNeedDrain)]: false,

[Symbol(corked)]: 0,

[Symbol(kOutHeaders)]: [Object: null prototype]

},

[Symbol(async_id_symbol)]: 3163,

[Symbol(async_id_symbol)]: 3163,

[Symbol(kHandle)]: TCP {

reading: true,

onconnection: null,

_consumed: true,

[Symbol(owner_symbol)]: [Circular *1]

},

[Symbol(kSetNoDelay)]: false,

[Symbol(lastWriteQueueSize)]: 0,

[Symbol(timeout)]: null,

[Symbol(kBuffer)]: null,

[Symbol(kBufferCb)]: null,

[Symbol(kBufferGen)]: null,

[Symbol(kCapture)]: false,

[Symbol(kBytesRead)]: 0,

[Symbol(kBytesWritten)]: 0,

[Symbol(RequestTimeout)]: undefined

},

_consuming: false

_dumped: false,

next: [Function: next],

baseUrl: '',

originalUrl: '/kakaoSvrNoti',

_parsedUrl: Url {

protocol: null

slashes: null,

auth: null,

host: null,

port: null,

hostname: null,

hash: null,

search: null,

query: null,

pathname: '/kakaoSvrNoti',

path: '/kakaoSvrNoti',

href: '/kakaoSvrNoti',

_raw: '/kakaoSvrNoti'

},

params: {},

query: {},

res: <ref *3> ServerResponse {

_events: [Object: null prototype] { finish: [Function: bound resOnFinish] },

_eventsCount: 1,

_maxListeners: undefined,

outputData: [],

outputSize: 0,

writable: true,

destroyed: false,

_last: false,

chunkedEncoding: false,

shouldKeepAlive: true,

_defaultKeepAlive: true,

useChunkedEncodingByDefault: true,

sendDate: true,

_removedConnection: false,

_removedContLen: false,

_removedTE: false,

_contentLength: null,

_hasBody: true,

_trailer: '',

finished: false,

_headerSent: false,

socket: <ref *1> Socket {

connecting: false,

_hadError: fals

_parent: null,

_host: null,

_readableState: [ReadableState],

_events: [Object: null prototype],

_eventsCount: 8,

_maxListeners: undefined,

_writableState: [WritableState],

allowHalfOpen: true,

_sockname: null,

_pendingData: null,

_pendingEncoding: '',

server: [Server],

_server: [Server],

parser: [HTTPParser],

on: [Function: socketListenerWrap],

addListener: [Function: socketListenerWrap],

prependListener: [Function: socketListenerWrap],

_paused: false,

_httpMessage: [Circular *3],

[Symbol(async_id_symbol)]: 3163,

[Symbol(kHandle)]: [TCP],

[Symbol(kSetNoDelay)]: false,

[Symbol(lastWriteQueueSize)]: 0,

[Symbol(timeout)]: null,

[Symbol(kBuffer)]: null,

[Symbol(kBufferCb)]: null,

[Symbol(kBufferGen)]: null,

[Symbol(kCapture)]: false,

[Symbol(kBytesRead)]: 0

[Symbol(kBytesWritten)]: 0,

[Symbol(RequestTimeout)]: undefined

},

_header: null,

_keepAliveTimeout: 5000

_onPendingData: [Function: bound updateOutgoingData],

_sent100: false,

_expect_continue: false,

req: [Circular *2],

locals: [Object: null pototype] {},

end: [Function (anonymous)],

[Symbol(kCapture)]: false,

[Symbol(kNeedDrain)]: false,

[Symbol(corked)]: 0,

[Symbol(kOutHeaders)]: [Object: null prototype] {

vary: [Array],

'access-control-allow-credentials': [Array],

'content-security-policy': [Array],

'x-dns-prefetch-control': [Array],

'expect-ct': [Array],

'x-frame-options': [Array],

'strict-transport-security': [Array],

'x-download-options': [Array],

'x-content-type-options': [Array],

'x-permitted-cross-domain-policies': [Array],

'referrer-policy': [Array],

'x-xss-protection': [Array]

}

},

_passport: {

instance: Authenticator {

_key: 'passport',

_strategies: [Object],

_serializers: [Array],

_deserializers: [Array],

_infoTransformers: [],

_framework: [Object],

_userProperty: 'user',

_sm: [SessionManager],

Authenticator: [Function: Authenticator],

Passport: [Function: Authenticator],

Strategy: [Function],

strategies: [Object]

}

},

body: {},

secret: undefined,

cookies: [Object: null prototype] {},

signedCookies: [Object: null prototype] {},

_startTime: 2023-07-24T01:41:17.868Z,

_routeWhitelists: { req: [], res: [], body: [] },

_routeBlacklists: { body: [] },

route: Route {

path: '/kakaoSvrNoti',

stack: [ [Layer] ],

methods: { post: true }

},

[Symbol(kCapture)]: false,

[Symbol(RequestTimeout)]: undefined

}

안녕하세요.

오늘 10:41 부터 정상 수신하시는 것으로 보입니다.
req.body 내용 확인 부탁드립니다.

res.status(202).end() 구문을 추가하여 수신에 대해 다시 처리하였고
그래도 아직 body에 찍히는 값이 없는데 위에 로그 확인해보시면 req.body 에 {} 빈 객체로만 확인이 됩니다.

제가 궁금한점은 secevent+jwt 타입에 대해 서버에서 따로 파싱 옵션을 주고 있지 않은데 해당 타입을 파싱 하기 위해서 옵션을 따로 줘야하는건가요?

아래처럼 ContentType에 대해 text 디코더를 지정해 보시겠어요?

app.use('/event', express.text({type: 'application/secevent+jwt'}), (req, res)=> {
  console.log(req.body)
}

미들웨어에 bodyparser.text({ type: 'application/secevent+jwt }) 로 추가하여 파싱 성공하였습니다. 감사합니다

1개의 좋아요