생각보다 험난했다 ㅎ.. slack bot을 만들어서 개인 workspace 에 귀속시켜서 사용하는 것은 간단하나 public하게 distribution 하기 위해서는 OAuth를 무조건 해야한다. 그러기 위한 re-direct url (call back url) 설정과 token관련 설정들 까지! slack bot create 부터 distribution 을 살펴보자.
깃허브에서 보기 : Dev-Event-Slack-Bot
Create New App
을 하면 된다. 그리고 From scratch
로 직접 step by step 설정을 하는게 편하다. 그 이후는 다른 많은 글을 살펴보는 게 낫다. 제일 중요하게 살펴봐야 할 부분은 "Bot Token Scopes" 부분이다.
깃허브 코드 를 보면 slack_token 값이 중요하다. slack sdk 또는 rest api 를 활용할땐 만든 APP의 OAuth & Permissions > OAuth Token
이 중요하다.
dev-event-slack-bot 이 사용하는 scope 들이다. slack bot이 메시징 보내는 로직은 추가한 채널 정보를 읽고, 해당 채널 정보 기반으로 우리가 chat을 write 해야한다. 생각보다 쓸모없는 scope 도 추가해줘야 했다.
깃허브 slack sdk 사용하는 코드 를 보면 왜 위와 같은 scope가 필요한지 확인이 가능하다. 사실 scope를 더 줄일 수 있었으나, 추가 확장성을 위해 optional한 method를 추가한다고 추가 scope가 따라왔다.
그리고 추후에 slack 에게 public 하게 인정받은 APP 이 되려면 해당 scope에 모두 각각 적절한 이유가 필요하다.
생각보다 OAuth 세팅을 하는데 좀 많이 헤멧다. token값을 어떤걸 사용해야 하는지, callback url (redirect url)에서 들어오는 데이터로 어떻게 인증해야하는지 애매했다.
이 부분 때문에 테스트를 하기도 난해하다. 아 물론 localhost에 local전용 SSL 인증서를 만들어서 하는 방법도 있지만 번거롭다. 그래서 선택한 방법이 공짜 도메인 + [nginx + certbot] 조합이다.
nginx 와 certbot 조합은 알사람은 이미 많이 아는, Let’s Encrypt 를 자동으로 발급/갱신 해주는 써드파티 S/W 이다. Let’s Encrypt는 인증서 요청 -> 도메인에 대한 소유권 확인 -> 발급 의 과정을 거치고, 더 자세한 내용은 클릭해서 확인하길 바란다. 그리고 더 자세한 certbot 활용법은 여기에서 확인 하면 좋다.
깃허브 repo의 nginx-certbot
디렉토리 모두가 이를 위해 존재한다. compose 파일에서 nginx image, certbot image를 사용 했고, docker compose -f docker-compose.yml -p dev-event-bot-nginx up -d
와 같은 command로 run 한 뒤에 같은 경로에 존재하는 init-letsencrypt.sh
를 실행시켜주면 된다.
해당 init shell
은 https://github.com/wmnnd/nginx-certbot 를 참조해서 만들어졌다. 핵심은 pem 파일 생성 후 open ssl을 활용해 유효성 검사까지 쭉 해줍니다. 해당 shell을 실행하고 나면 (경로가 같다면) nginx-certbot/data/certbot/
하위에 conf
와 www
디렉토리가 추가 생성된다!
이제 domain name + ssl (https) 는 simple한 상태로 (엔터프라이즈 환경에서는 주의가 필요!!) test 와 간단한 운영 레벨 정도까지는 가능하다.
일단 코어가 python기반 플젝인 flask로 할까 하다가, 앱 배포를 위해 example이 가장 많고 빠르게 도입할 수 있는게 무엇이 있을까 하다가 node - express
로 결정했다. 그리고 simple한 landing이 있어야 한다. (더 자세하게는 "slack이 인증한 APP이 되려면 APP scope와 landing 등 모든 것에 대한 세부 설명이 필요"하다. 그게 아니라면 공식 인증 APP이 아니라고 뜬다.)
사실 landing이 필수가 아니기 때문에 위에서 사용할 "redirect url" 에 request를 받아줄 rest API 하나만 있으면 된다. 일단 위 https + ssl과 redirect url restAPI 구성을 위해 전체 디렉토리 구성은 아래와 같이 되었다.
├── core
│ ├── __init__.py
│ ├── config.py
│ ├── db
│ │ └── mongodb.py
│ ├── exception_handler.py
│ └── slack.py
├── crawler
│ ├── __init__.py
│ └── crawler.py
├── nginx-certbot
│ ├── README.md
│ ├── data
│ │ ├── certbot
│ │ └── nginx
│ ├── docker-compose.yml
│ └── init-letsencrypt.sh
├── node_modules
├── oauth
│ ├── app.js
│ ├── node_modules
│ ├── package.json
│ ├── public
│ ├── routes
│ ├── views
│ ├── www
│ └── yarn.lock
├── main.py
├── requirements.txt
├── runtime.txt
└── secrets.json
oauth에 저렇게 많이 주렁주렁 달려있을 필요가 없다. 실제 필요한 API도 단일이라 그냥 app.js에 다 때려박아도 된다. git hub repo에서 oauth > www
와 oauth > app.js
내용을 참고하길 바란다. API만 다뤄볼 것이다.
아래는 oauth > routes > index.js
파일이다.
const { WebClient } = require('@slack/web-api');
const client = new WebClient();
router.get('/auth/slack', async (_, res) => {
const botScopes = 'calls:write,chat:write,channels:read,groups:read,mpim:read,im:read';
const userScopes = '';
const clientId = process.env.SLACK_CLIENT_ID;
const oauthUrl = `https://slack.com/oauth/v2/authorize?client_id=${clientId}&scope=${botScopes}&user_scope=${userScopes}`;
return res.render('index', { oauthUrl });
});
router.get('/auth/slack/callback', async (req, res) => {
try {
const response = await client.oauth.v2.access({
client_id: process.env.SLACK_CLIENT_ID,
client_secret: process.env.SLACK_CLIENT_SECRET,
code: req.query.code,
});
console.dir(response);
// At this point you can assume the user has logged in successfully with their account.
return res.status(200).send(`<html><body><p>You have successfully logged in with your slack account! Here are the details:</p><p>Response: ${JSON.stringify(response)}</p></body></html>`);
} catch (eek) {
console.log(eek);
return res.status(500).send(`<html><body><p>Something went wrong!</p><p>${JSON.stringify(eek)}</p>`);
}
});
/auth/slack
API는 사실 필요없는 API이다. ejs view engine을 활용해서 단순하게 /auth/slack/callback
로 가는 버튼하나 있을 뿐이다. 핵심은 여기서 세팅해주는 그 버튼의 url인데, 공식 홈페이지에서 확인 가능하다.
slack 엡페이지 > Settings > Basic information > App Credentials 색션
에서 확인가능한 "Client ID" 값이 clientId 이며 url에서는 bot, user scope값이 굉장히 중요하다. client id는 url로 노출되기 때문에 엄청나게 중요한 비밀값은 아니다.
그리고 bot이랑 user scope를 헷갈리면 절대 안된다. scope값이 다르면 install 자체에서 error를 엄청 뱉게되어 있다. user scope는 user 자체를 OAuth 로 해당 workspace로 데리고오는 것이다. 그 user에게 부여되는 Auth에 관한 값이 user scope이다. 지금 목적에서는, 우리는 bot만 신경쓰면 된다.
/auth/slack/callback
에서 처리가 정말 중요하다. slack에서 제공해주는 js sdk @slack/web-api
를 활용해서 처리하는게 좋다. client.oauth.v2.access
로 redirect를 통해 넘어온 request를 처리한다. "code"라는 query string을 넘기는데, 해당값은 slack에서 조합해서 넘겨주는 값이다. 우리가 신경쓸 값은 SLACK_CLIENT_SECRET
값이다.
SLACK_CLIENT_SECRET 이 값때문에 고생했는데, slack에서 token값이 워낙 많기 때문에 정확하게 차이를 알아야한다.
crawler에서 bot에게 자기를 추가한 모든 채널에 message를 보내라는 slack API를 사용할 때는 slack 엡페이지 > Features > OAuth & Permissions
에서 OAuth Tokens for Your Workspace
색션에서 Bot User OAuth Token
값이 필요하다. 우리가 bot을 제어해서 slack API 를 사용하기 때문이다.
SLACK_CLIENT_SECRET은 bot install을 위해 OAuth가 필요한 상황이다. slack 엡페이지 > Settings > Basic information > App Credentials 색션
에서 아래 사진과 같은 부분이 필요하다.
그리고 이건oauth.v2.access request
만을 위해서 사용되는 비밀값이다. 노출되면 안된다!
만약 user 대상으로 workspace OAuth를 만들고 있다면, user scope 값이랑 위 (5)에서 언급한 Bot User OAuth Token 이 아니라 User OAuth Token 값이 필요할 수 있다.
그리고 client.oauth.v2.access
가 정상적으로 처리가 되었으면 저렇게 처리하지말고, Deeplink - URI 스킴 방식 : 앱에 URI 스킴(scheme), slack의 스킴 slack://
을 활용하면 더욱 깔끔하게 처리가 가능하다.
You have successfully logged in with your slack account! Here are the details:
Response:
{"ok":true,"app_id":"비공개처리","authed_user":{"id":"비공개처리"},
"scope":"calls:write,chat:write,channels:read,groups:read,mpim:read,im:read",
"token_type":"bot","access_token":"비공개처리","bot_user_id":"비공개처리",
"team":{"id":"비공개처리","name":"amnotifyKR"},"enterprise":null,
"is_enterprise_install":false,"response_metadata":{}}
url에서 token을 잘못 세팅했으면 위와 같이 뜬다. 그 외에 아주 다양한 에러를 살펴볼 수 있는데 꼭 scope 값을 확인하기 바란다!!
그리고 https://slack.com/intl/ko-kr/blog/developers/slack-app-directory-review-process 에서 완전하게 slack한테 인증받기위한 과정을 살펴보자
아직은 베타라 테스트성 메시지를 수신할 수 있습니다.
아직 완벽한 Public 권한을 받지 못한 상태입니다. (2022.11.15 기준)