Skip to content

Commit f68f607

Browse files
authored
feat: more social sign in platforms
1 parent f5161d9 commit f68f607

File tree

14 files changed

+390
-47
lines changed

14 files changed

+390
-47
lines changed

api/.env.example

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,31 @@ SENDGRID_API_KEY=
384384
# Sentry configuration
385385
SENTRY_DSN=
386386

387+
# ------------------------------
388+
# OAuth and Authentication Configuration
389+
# ------------------------------
390+
391+
# GitHub OAuth configuration
392+
GITHUB_CLIENT_ID=
393+
GITHUB_CLIENT_SECRET=
394+
395+
# Google OAuth configuration
396+
GOOGLE_CLIENT_ID=
397+
GOOGLE_CLIENT_SECRET=
398+
399+
# DingTalk OAuth configuration
400+
DINGTALK_CLIENT_ID=
401+
DINGTALK_CLIENT_SECRET=
402+
403+
# Microsoft OAuth configuration
404+
MICROSOFT_CLIENT_ID=
405+
MICROSOFT_CLIENT_SECRET=
406+
407+
# Canvas LMS OAuth configuration
408+
CANVAS_CLIENT_ID=
409+
CANVAS_CLIENT_SECRET=
410+
CANVAS_INSTALL_URL=
411+
387412
# DEBUG
388413
DEBUG=false
389414
ENABLE_REQUEST_LOGGING=False

api/configs/feature/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,41 @@ class AuthConfig(BaseSettings):
684684
default=None,
685685
)
686686

687+
DINGTALK_CLIENT_ID: str | None = Field(
688+
description="DingTalk OAuth client ID",
689+
default=None,
690+
)
691+
692+
DINGTALK_CLIENT_SECRET: str | None = Field(
693+
description="DingTalk OAuth client secret",
694+
default=None,
695+
)
696+
697+
MICROSOFT_CLIENT_ID: str | None = Field(
698+
description="Microsoft OAuth client ID",
699+
default=None,
700+
)
701+
702+
MICROSOFT_CLIENT_SECRET: str | None = Field(
703+
description="Microsoft OAuth client secret",
704+
default=None,
705+
)
706+
707+
CANVAS_CLIENT_ID: str | None = Field(
708+
description="Canvas OAuth client ID",
709+
default=None,
710+
)
711+
712+
CANVAS_CLIENT_SECRET: str | None = Field(
713+
description="Canvas OAuth client secret",
714+
default=None,
715+
)
716+
717+
CANVAS_INSTALL_URL: str | None = Field(
718+
description="Canvas installation URL (e.g., https://canvas.instructure.com)",
719+
default=None,
720+
)
721+
687722
ACCESS_TOKEN_EXPIRE_MINUTES: PositiveInt = Field(
688723
description="Expiration time for access tokens in minutes",
689724
default=60,

api/controllers/console/auth/oauth.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@
1313
from extensions.ext_database import db
1414
from libs.datetime_utils import naive_utc_now
1515
from libs.helper import extract_remote_ip
16-
from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo
16+
from libs.oauth import (
17+
CanvasOAuth,
18+
DingTalkOAuth,
19+
GitHubOAuth,
20+
GoogleOAuth,
21+
MicrosoftOAuth,
22+
OAuthUserInfo,
23+
)
1724
from models import Account
1825
from models.account import AccountStatus
1926
from services.account_service import AccountService, RegisterService, TenantService
@@ -29,25 +36,54 @@
2936

3037
def get_oauth_providers():
3138
with current_app.app_context():
32-
if not dify_config.GITHUB_CLIENT_ID or not dify_config.GITHUB_CLIENT_SECRET:
33-
github_oauth = None
34-
else:
35-
github_oauth = GitHubOAuth(
39+
providers = {}
40+
41+
# GitHub
42+
if dify_config.GITHUB_CLIENT_ID and dify_config.GITHUB_CLIENT_SECRET:
43+
providers["github"] = GitHubOAuth(
3644
client_id=dify_config.GITHUB_CLIENT_ID,
3745
client_secret=dify_config.GITHUB_CLIENT_SECRET,
3846
redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/github",
3947
)
40-
if not dify_config.GOOGLE_CLIENT_ID or not dify_config.GOOGLE_CLIENT_SECRET:
41-
google_oauth = None
42-
else:
43-
google_oauth = GoogleOAuth(
48+
49+
# Google
50+
if dify_config.GOOGLE_CLIENT_ID and dify_config.GOOGLE_CLIENT_SECRET:
51+
providers["google"] = GoogleOAuth(
4452
client_id=dify_config.GOOGLE_CLIENT_ID,
4553
client_secret=dify_config.GOOGLE_CLIENT_SECRET,
4654
redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/google",
4755
)
4856

49-
OAUTH_PROVIDERS = {"github": github_oauth, "google": google_oauth}
50-
return OAUTH_PROVIDERS
57+
# DingTalk
58+
if dify_config.DINGTALK_CLIENT_ID and dify_config.DINGTALK_CLIENT_SECRET:
59+
providers["dingtalk"] = DingTalkOAuth(
60+
client_id=dify_config.DINGTALK_CLIENT_ID,
61+
client_secret=dify_config.DINGTALK_CLIENT_SECRET,
62+
redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/dingtalk",
63+
)
64+
65+
# Microsoft
66+
if dify_config.MICROSOFT_CLIENT_ID and dify_config.MICROSOFT_CLIENT_SECRET:
67+
providers["microsoft"] = MicrosoftOAuth(
68+
client_id=dify_config.MICROSOFT_CLIENT_ID,
69+
client_secret=dify_config.MICROSOFT_CLIENT_SECRET,
70+
redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/microsoft",
71+
)
72+
73+
# Canvas
74+
if (
75+
dify_config.CANVAS_CLIENT_ID
76+
and dify_config.CANVAS_CLIENT_SECRET
77+
and dify_config.CANVAS_INSTALL_URL
78+
):
79+
providers["canvas"] = CanvasOAuth(
80+
client_id=dify_config.CANVAS_CLIENT_ID,
81+
client_secret=dify_config.CANVAS_CLIENT_SECRET,
82+
redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/canvas",
83+
install_url=dify_config.CANVAS_INSTALL_URL,
84+
)
85+
86+
return providers
5187

5288

5389
@console_ns.route("/oauth/login/<provider>")

api/libs/oauth.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,161 @@ def get_raw_user_info(self, token: str):
130130

131131
def _transform_user_info(self, raw_info: dict) -> OAuthUserInfo:
132132
return OAuthUserInfo(id=str(raw_info["sub"]), name="", email=raw_info["email"])
133+
134+
135+
class DingTalkOAuth(OAuth):
136+
"""DingTalk OAuth implementation"""
137+
138+
_AUTH_URL = "https://login.dingtalk.com/oauth2/auth"
139+
_TOKEN_URL = "https://api.dingtalk.com/v1.0/oauth2/userAccessToken"
140+
_USER_INFO_URL = "https://api.dingtalk.com/v1.0/contact/users/me"
141+
142+
def get_authorization_url(self, invite_token: str | None = None):
143+
params = {
144+
"client_id": self.client_id,
145+
"redirect_uri": self.redirect_uri,
146+
"scope": "openid",
147+
"response_type": "code",
148+
"prompt": "consent",
149+
}
150+
if invite_token:
151+
params["state"] = invite_token
152+
return f"{self._AUTH_URL}?{urllib.parse.urlencode(params)}"
153+
154+
def get_access_token(self, code: str):
155+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
156+
data = {
157+
"clientId": self.client_id,
158+
"clientSecret": self.client_secret,
159+
"code": code,
160+
"grantType": "authorization_code",
161+
}
162+
response = httpx.post(self._TOKEN_URL, json=data, headers=headers)
163+
response_json = response.json()
164+
access_token = response_json.get("accessToken")
165+
166+
if not access_token:
167+
raise ValueError(f"Error in DingTalk OAuth: {response_json}")
168+
169+
return access_token
170+
171+
def get_raw_user_info(self, token: str):
172+
headers = {"x-acs-dingtalk-access-token": token}
173+
response = httpx.get(self._USER_INFO_URL, headers=headers)
174+
response.raise_for_status()
175+
return response.json()
176+
177+
def _transform_user_info(self, raw_info: dict) -> OAuthUserInfo:
178+
user_id = raw_info.get("unionId", "")
179+
name = raw_info.get("nick", "")
180+
email = raw_info.get("email", f"{user_id}@dingtalk.com")
181+
return OAuthUserInfo(id=user_id, name=name, email=email)
182+
183+
184+
class MicrosoftOAuth(OAuth):
185+
"""Microsoft OAuth implementation"""
186+
187+
_AUTH_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
188+
_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
189+
_USER_INFO_URL = "https://graph.microsoft.com/v1.0/me"
190+
191+
def get_authorization_url(self, invite_token: str | None = None):
192+
params = {
193+
"client_id": self.client_id,
194+
"redirect_uri": self.redirect_uri,
195+
"response_type": "code",
196+
"scope": "user.read",
197+
"response_mode": "query",
198+
}
199+
if invite_token:
200+
params["state"] = invite_token
201+
return f"{self._AUTH_URL}?{urllib.parse.urlencode(params)}"
202+
203+
def get_access_token(self, code: str):
204+
data = {
205+
"client_id": self.client_id,
206+
"client_secret": self.client_secret,
207+
"code": code,
208+
"grant_type": "authorization_code",
209+
"redirect_uri": self.redirect_uri,
210+
}
211+
headers = {"Accept": "application/json"}
212+
response = httpx.post(self._TOKEN_URL, data=data, headers=headers)
213+
response_json = response.json()
214+
access_token = response_json.get("access_token")
215+
216+
if not access_token:
217+
raise ValueError(f"Error in Microsoft OAuth: {response_json}")
218+
219+
return access_token
220+
221+
def get_raw_user_info(self, token: str):
222+
headers = {"Authorization": f"Bearer {token}"}
223+
response = httpx.get(self._USER_INFO_URL, headers=headers)
224+
response.raise_for_status()
225+
return response.json()
226+
227+
def _transform_user_info(self, raw_info: dict) -> OAuthUserInfo:
228+
user_id = str(raw_info.get("id", ""))
229+
name = raw_info.get("displayName", "")
230+
email = raw_info.get("mail") or raw_info.get("userPrincipalName", f"{user_id}@microsoft.com")
231+
return OAuthUserInfo(id=user_id, name=name, email=email)
232+
233+
234+
class CanvasOAuth(OAuth):
235+
"""Canvas LMS OAuth implementation"""
236+
237+
def __init__(self, client_id: str, client_secret: str, redirect_uri: str, install_url: str):
238+
super().__init__(client_id, client_secret, redirect_uri)
239+
self.install_url = install_url.rstrip("/")
240+
self._AUTH_URL = f"{self.install_url}/login/oauth2/auth"
241+
self._TOKEN_URL = f"{self.install_url}/login/oauth2/token"
242+
self._user_cache = None # Cache user info from token response
243+
244+
def get_authorization_url(self, invite_token: str | None = None):
245+
params = {
246+
"client_id": self.client_id,
247+
"redirect_uri": self.redirect_uri,
248+
"response_type": "code",
249+
}
250+
if invite_token:
251+
params["state"] = invite_token
252+
return f"{self._AUTH_URL}?{urllib.parse.urlencode(params)}"
253+
254+
def get_access_token(self, code: str):
255+
data = {
256+
"client_id": self.client_id,
257+
"client_secret": self.client_secret,
258+
"code": code,
259+
"grant_type": "authorization_code",
260+
"redirect_uri": self.redirect_uri,
261+
}
262+
headers = {"Accept": "application/json"}
263+
response = httpx.post(self._TOKEN_URL, data=data, headers=headers)
264+
response_json = response.json()
265+
266+
# Canvas returns user info in the token response
267+
# Example: {"access_token": "...", "user": {"id": 42, "name": "Jimi Hendrix"}, ...}
268+
user_info = response_json.get("user", {})
269+
if user_info:
270+
self._user_cache = user_info
271+
272+
access_token = response_json.get("access_token")
273+
if not access_token:
274+
raise ValueError(f"Error in Canvas OAuth: {response_json}")
275+
276+
return access_token
277+
278+
def get_raw_user_info(self, token: str):
279+
# Canvas returns user info in the token response, which we cached
280+
if self._user_cache:
281+
return self._user_cache
282+
283+
raise ValueError("No user info available.")
284+
285+
def _transform_user_info(self, raw_info: dict) -> OAuthUserInfo:
286+
# Canvas uses 'id' as the primary identifier
287+
user_id = str(raw_info.get("id", ""))
288+
name = raw_info.get("name", "")
289+
email = raw_info.get("email", f"{user_id}@canvas.local")
290+
return OAuthUserInfo(id=user_id, name=name, email=email)

api/services/feature_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ class SystemFeatureModel(BaseModel):
160160
plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel()
161161
enable_change_email: bool = True
162162
plugin_manager: PluginManagerModel = PluginManagerModel()
163+
oauth_providers: list[str] = []
163164

164165

165166
class FeatureService:
@@ -208,12 +209,18 @@ def get_system_features(cls) -> SystemFeatureModel:
208209

209210
@classmethod
210211
def _fulfill_system_params_from_env(cls, system_features: SystemFeatureModel):
212+
from controllers.console.auth.oauth import get_oauth_providers
213+
211214
system_features.enable_email_code_login = dify_config.ENABLE_EMAIL_CODE_LOGIN
212215
system_features.enable_email_password_login = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN
213216
system_features.enable_social_oauth_login = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN
214217
system_features.is_allow_register = dify_config.ALLOW_REGISTER
215218
system_features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE
216219
system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != ""
220+
221+
# Populate oauth_providers using the get_oauth_providers function
222+
oauth_providers_dict = get_oauth_providers()
223+
system_features.oauth_providers = list(oauth_providers_dict.keys())
217224

218225
@classmethod
219226
def _fulfill_params_from_env(cls, features: FeatureModel):

docker/.env.example

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,31 @@ NOTION_CLIENT_ID=
812812
# you need to configure this variable.
813813
NOTION_INTERNAL_SECRET=
814814

815+
# ------------------------------
816+
# OAuth and Authentication Configuration
817+
# ------------------------------
818+
819+
# GitHub OAuth configuration
820+
GITHUB_CLIENT_ID=
821+
GITHUB_CLIENT_SECRET=
822+
823+
# Google OAuth configuration
824+
GOOGLE_CLIENT_ID=
825+
GOOGLE_CLIENT_SECRET=
826+
827+
# DingTalk OAuth configuration
828+
DINGTALK_CLIENT_ID=
829+
DINGTALK_CLIENT_SECRET=
830+
831+
# Microsoft OAuth configuration
832+
MICROSOFT_CLIENT_ID=
833+
MICROSOFT_CLIENT_SECRET=
834+
835+
# Canvas LMS OAuth configuration
836+
CANVAS_CLIENT_ID=
837+
CANVAS_CLIENT_SECRET=
838+
CANVAS_INSTALL_URL=
839+
815840
# ------------------------------
816841
# Mail related configuration
817842
# ------------------------------

docker/docker-compose.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,17 @@ x-shared-env: &shared-api-worker-env
363363
NOTION_CLIENT_SECRET: ${NOTION_CLIENT_SECRET:-}
364364
NOTION_CLIENT_ID: ${NOTION_CLIENT_ID:-}
365365
NOTION_INTERNAL_SECRET: ${NOTION_INTERNAL_SECRET:-}
366+
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
367+
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
368+
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
369+
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
370+
DINGTALK_CLIENT_ID: ${DINGTALK_CLIENT_ID:-}
371+
DINGTALK_CLIENT_SECRET: ${DINGTALK_CLIENT_SECRET:-}
372+
MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}
373+
MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}
374+
CANVAS_CLIENT_ID: ${CANVAS_CLIENT_ID:-}
375+
CANVAS_CLIENT_SECRET: ${CANVAS_CLIENT_SECRET:-}
376+
CANVAS_INSTALL_URL: ${CANVAS_INSTALL_URL:-}
366377
MAIL_TYPE: ${MAIL_TYPE:-resend}
367378
MAIL_DEFAULT_SEND_FROM: ${MAIL_DEFAULT_SEND_FROM:-}
368379
RESEND_API_URL: ${RESEND_API_URL:-https://api.resend.com}

web/app/signin/assets/canvas.svg

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)