summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorge Hazan <george.hazan@gmail.com>2025-03-30 13:46:07 +0300
committerGeorge Hazan <george.hazan@gmail.com>2025-03-30 13:46:07 +0300
commit1f302e539d9120302fe018b4398e65e658605669 (patch)
treea009d87ee0778c3c9afedfa98b3c87a56924882d
parent09aee2a73b7f15cadcb48fcc433f30b256742cc6 (diff)
Teams: device code auth implementation
-rw-r--r--protocols/Teams/res/Resource.rc56
-rw-r--r--protocols/Teams/src/proto.h31
-rw-r--r--protocols/Teams/src/resource.h6
-rw-r--r--protocols/Teams/src/teams_login.cpp142
-rw-r--r--protocols/Teams/src/teams_proto.cpp1
5 files changed, 220 insertions, 16 deletions
diff --git a/protocols/Teams/res/Resource.rc b/protocols/Teams/res/Resource.rc
index 189c8cba6a..5aba8192d5 100644
--- a/protocols/Teams/res/Resource.rc
+++ b/protocols/Teams/res/Resource.rc
@@ -131,6 +131,62 @@ END
/////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Dialog
+//
+
+IDD_DEVICECODE DIALOGEX 0, 0, 316, 182
+STYLE DS_SETFONT | DS_SETFOREGROUND | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_SYSMENU
+EXSTYLE WS_EX_APPWINDOW
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+ DEFPUSHBUTTON "Proceed",IDOK,205,161,50,14
+ PUSHBUTTON "Cancel",IDCANCEL,259,161,50,14
+ LTEXT "Static",IDC_TEXT,7,7,302,147
+END
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// DESIGNINFO
+//
+
+#ifdef APSTUDIO_INVOKED
+GUIDELINES DESIGNINFO
+BEGIN
+ IDD_DEVICECODE, DIALOG
+ BEGIN
+ LEFTMARGIN, 7
+ RIGHTMARGIN, 309
+ TOPMARGIN, 7
+ BOTTOMMARGIN, 175
+ END
+END
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// AFX_DIALOG_LAYOUT
+//
+
+IDD_DEVICECODE AFX_DIALOG_LAYOUT
+BEGIN
+ 0
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
diff --git a/protocols/Teams/src/proto.h b/protocols/Teams/src/proto.h
index de6a543bd9..aad0d47b7a 100644
--- a/protocols/Teams/src/proto.h
+++ b/protocols/Teams/src/proto.h
@@ -1,3 +1,5 @@
+#define TEAMS_CLIENT_ID "8ec6bc83-69c8-4392-8f08-b3c986009232"
+
enum HostType {
HOST_OTHER = 0,
HOST_LOGIN = 1,
@@ -13,6 +15,26 @@ struct AsyncHttpRequest : public MTHttpRequest<CTeamsProto>
class CTeamsProto : public PROTO<CTeamsProto>
{
friend class COptionsMainDlg;
+ friend class CDeviceCodeDlg;
+
+ class CTeamsProtoImpl
+ {
+ friend class CTeamsProto;
+ CTeamsProto &m_proto;
+
+ CTimer m_loginPoll;
+ void OnLoginPoll(CTimer *)
+ {
+ m_proto.LoginPoll();
+ }
+
+ CTeamsProtoImpl(CTeamsProto &pro) :
+ m_proto(pro),
+ m_loginPoll(Miranda_GetSystemWindow(), UINT_PTR(this) + 1)
+ {
+ m_loginPoll.OnEvent = Callback(this, &CTeamsProtoImpl::OnLoginPoll);
+ }
+ } m_impl;
CMStringA GetTenant();
@@ -22,6 +44,7 @@ class CTeamsProto : public PROTO<CTeamsProto>
LIST<AsyncHttpRequest> m_requests;
MEventHandle m_hRequestQueueEvent;
HANDLE m_hRequestQueueThread;
+ CMStringA m_szAccessToken;
void __cdecl WorkerThread(void *);
@@ -34,9 +57,17 @@ class CTeamsProto : public PROTO<CTeamsProto>
void PushRequest(AsyncHttpRequest *request);
// login
+ CMStringW m_wszUserCode;
+ CMStringA m_szDeviceCode, m_szDeviceCookie, m_szVerificationUrl;
+ time_t m_iLoginExpires;
+
void Login();
+ void LoginPoll();
+ void LoginError();
void RefreshToken(const char *pszScope, AsyncHttpRequest::MTHttpRequestHandler pFunc);
+ void OnReceiveDevicePoll(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnReceiveDeviceToken(MHttpResponse *response, AsyncHttpRequest *pRequest);
void OnRefreshAccessToken(MHttpResponse *response, AsyncHttpRequest *pRequest);
void OnRefreshSubstrate(MHttpResponse *response, AsyncHttpRequest *pRequest);
diff --git a/protocols/Teams/src/resource.h b/protocols/Teams/src/resource.h
index c9050aea02..dd0cecf12b 100644
--- a/protocols/Teams/src/resource.h
+++ b/protocols/Teams/src/resource.h
@@ -5,17 +5,19 @@
#define IDI_TEAMS 100
#define IDD_ACCOUNT_MANAGER 101
#define IDD_OPTIONS_MAIN 102
+#define IDD_DEVICECODE 103
#define IDC_LOGIN 1001
#define IDC_PASSWORD 1002
#define IDC_GROUP 1003
+#define IDC_TEXT 1004
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
-#define _APS_NEXT_RESOURCE_VALUE 128
+#define _APS_NEXT_RESOURCE_VALUE 104
#define _APS_NEXT_COMMAND_VALUE 40001
-#define _APS_NEXT_CONTROL_VALUE 1087
+#define _APS_NEXT_CONTROL_VALUE 1085
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
diff --git a/protocols/Teams/src/teams_login.cpp b/protocols/Teams/src/teams_login.cpp
index 9be1df5de8..b2b4be0a92 100644
--- a/protocols/Teams/src/teams_login.cpp
+++ b/protocols/Teams/src/teams_login.cpp
@@ -17,37 +17,120 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "stdafx.h"
+#define TEAMS_OAUTH_RESOURCE "https://api.spaces.skype.com"
+
CMStringA CTeamsProto::GetTenant()
{
CMStringA ret(getMStringA("Tenant"));
return (ret.IsEmpty()) ? "consumers" : ret;
}
-void CTeamsProto::Login()
+void CTeamsProto::LoginError()
{
- CMStringA szLogin(getMStringA("Login")), szPassword(getMStringA("Password"));
- if (szLogin.IsEmpty() || szPassword.IsEmpty()) {
- ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001);
+ ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001);
+ SetStatus(ID_STATUS_OFFLINE);
+
+ if (m_iLoginExpires) {
+ m_impl.m_loginPoll.StopSafe();
+ m_iLoginExpires = 0;
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CTeamsProto::OnReceiveDevicePoll(MHttpResponse *response, AsyncHttpRequest*)
+{
+ JsonReply reply(response);
+ if (!reply) {
+ if (!strstr(response->body, "\"error\":\"authorization_pending\""))
+ LoginError();
return;
}
+}
- // login
- int oldStatus = m_iStatus;
- m_iStatus = ID_STATUS_CONNECTING;
- ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus);
+void CTeamsProto::LoginPoll()
+{
+ if (time(0) >= m_iLoginExpires) {
+ LoginError();
+ return;
+ }
- StartQueue();
+ auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_LOGIN, "/common/oauth2/token", &CTeamsProto::OnReceiveDevicePoll);
+ pReq->AddHeader("Cookie", m_szDeviceCookie);
+ pReq << CHAR_PARAM("client_id", TEAMS_CLIENT_ID) << CHAR_PARAM("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
+ << CHAR_PARAM("code", m_szDeviceCode);
+ PushRequest(pReq);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+const wchar_t wszLoginMessage[] =
+ LPGENW("To login into Teams you need to open '%S' in a browser and select your Teams account there.") L"\r\n\r\n"
+ LPGENW("Enter the following code then: %s.") L"\r\n\r\n"
+ LPGENW("Click Proceed to copy that code to clipboard and launch a browser");
+
+class CDeviceCodeDlg : public CTeamsDlgBase
+{
+ bool bSucceeded = false;
+
+public:
+ CDeviceCodeDlg(CTeamsProto *ppro) :
+ CTeamsDlgBase(ppro, IDD_DEVICECODE)
+ {}
+
+ bool OnInitDialog() override
+ {
+ CMStringW wszText(FORMAT, TranslateW(wszLoginMessage), m_proto->m_szVerificationUrl.c_str(), m_proto->m_wszUserCode.c_str());
+ SetDlgItemTextW(m_hwnd, IDC_TEXT, wszText);
+ return true;
+ }
+
+ bool OnApply() override
+ {
+ bSucceeded = true;
+ Utils_OpenUrl(m_proto->m_szVerificationUrl);
+ return true;
+ }
- RefreshToken("service::api.fl.teams.microsoft.com::MBI_SSL openid profile offline_access", &CTeamsProto::OnRefreshAccessToken);
- // RefreshToken("service::api.fl.spaces.skype.com::MBI_SSL openid profile offline_access", &CTeamsProto::OnRefreshAccessToken);
- // RefreshToken("https://substrate.office.com/M365.Access openid profile offline_access", &CTeamsProto::OnRefreshSubstrate);
+ void OnDestroy() override
+ {
+ if (!bSucceeded)
+ m_proto->LoginError();
+ }
+};
+
+static void CALLBACK LaunchDialog(void *param)
+{
+ (new CDeviceCodeDlg((CTeamsProto *)param))->Show();
}
+void CTeamsProto::OnReceiveDeviceToken(MHttpResponse *response, AsyncHttpRequest*)
+{
+ JsonReply reply(response);
+ if (!reply) {
+ LoginError();
+ return;
+ }
+
+ auto &root = reply.data();
+ m_wszUserCode = root["user_code"].as_mstring();
+ m_szDeviceCode = root["device_code"].as_mstring();
+ m_szVerificationUrl = root["verification_url"].as_mstring();
+ m_iLoginExpires = time(0) + root["expires_in"].as_int();
+ m_impl.m_loginPoll.StartSafe(root["interval"].as_int() * 1000);
+ m_szDeviceCookie = response->GetCookies();
+
+ Utils_ClipboardCopy(MClipUnicode(m_wszUserCode));
+ CallFunctionAsync(LaunchDialog, this);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
void CTeamsProto::RefreshToken(const char *pszScope, AsyncHttpRequest::MTHttpRequestHandler pFunc)
{
auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_LOGIN, "/" + GetTenant() + "/oauth2/v2.0/token", pFunc);
- pReq << CHAR_PARAM("scope", pszScope) << CHAR_PARAM("client_id", "8ec6bc83-69c8-4392-8f08-b3c986009232")
- << CHAR_PARAM("grant_type", "refresh_token") << CHAR_PARAM("refresh_token", getMStringA("Password"));
+ pReq << CHAR_PARAM("scope", pszScope) << CHAR_PARAM("client_id", TEAMS_CLIENT_ID)
+ << CHAR_PARAM("grant_type", "refresh_token") << CHAR_PARAM("refresh_token", m_szAccessToken);
PushRequest(pReq);
}
@@ -56,3 +139,34 @@ void CTeamsProto::OnRefreshAccessToken(MHttpResponse *response, AsyncHttpRequest
void CTeamsProto::OnRefreshSubstrate(MHttpResponse *response, AsyncHttpRequest *pRequest)
{}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// module entry point
+
+void CTeamsProto::Login()
+{
+ CMStringA szLogin(getMStringA("Login")), szPassword(getMStringA("Password"));
+ if (szLogin.IsEmpty() || szPassword.IsEmpty()) {
+ LoginError();
+ return;
+ }
+
+ // login
+ int oldStatus = m_iStatus;
+ m_iStatus = ID_STATUS_CONNECTING;
+ ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus);
+
+ StartQueue();
+
+ m_szAccessToken = getMStringA("AccessToken");
+ if (m_szAccessToken.IsEmpty()) {
+ auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_LOGIN, "/common/oauth2/devicecode", &CTeamsProto::OnReceiveDeviceToken);
+ pReq << CHAR_PARAM("client_id", TEAMS_CLIENT_ID) << CHAR_PARAM("resource", TEAMS_CLIENT_ID);
+ PushRequest(pReq);
+ }
+ else {
+ RefreshToken("service::api.fl.teams.microsoft.com::MBI_SSL openid profile offline_access", &CTeamsProto::OnRefreshAccessToken);
+ // RefreshToken("service::api.fl.spaces.skype.com::MBI_SSL openid profile offline_access", &CTeamsProto::OnRefreshAccessToken);
+ // RefreshToken("https://substrate.office.com/M365.Access openid profile offline_access", &CTeamsProto::OnRefreshSubstrate);
+ }
+}
diff --git a/protocols/Teams/src/teams_proto.cpp b/protocols/Teams/src/teams_proto.cpp
index 0cf9fb0e1c..30a942fd99 100644
--- a/protocols/Teams/src/teams_proto.cpp
+++ b/protocols/Teams/src/teams_proto.cpp
@@ -2,6 +2,7 @@
CTeamsProto::CTeamsProto(const char *protoName, const wchar_t *userName) :
PROTO<CTeamsProto>(protoName, userName),
+ m_impl(*this),
m_requests(10),
m_wstrCListGroup(this, "DefaultGroup", L"Teams")
{