Browse Source
Merge pull request #2079 from botisan-ai/okta-login
Added Okta Login for newer versions of Doccano
pull/2094/head
Hiroki Nakayama
1 year ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with
146 additions and
8 deletions
-
backend/config/settings/base.py
-
backend/config/urls.py
-
backend/pyproject.toml
-
backend/social/__init__.py
-
backend/social/admin.py
-
backend/social/apps.py
-
backend/social/models.py
-
backend/social/okta.py
-
backend/social/urls.py
-
backend/social/v1_urls.py
-
backend/social/views.py
-
frontend/components/auth/SocialLogin.vue
-
frontend/domain/models/auth/authRepository.ts
-
frontend/i18n/de/user.js
-
frontend/i18n/en/user.js
-
frontend/i18n/fr/user.js
-
frontend/i18n/zh/user.js
-
frontend/pages/auth.vue
-
frontend/repositories/auth/apiAuthRepository.ts
-
frontend/services/application/auth/authApplicationService.ts
-
frontend/store/auth.js
|
|
@ -59,8 +59,10 @@ INSTALLED_APPS = [ |
|
|
|
"allauth", |
|
|
|
"allauth.account", |
|
|
|
"allauth.socialaccount", |
|
|
|
"allauth.socialaccount.providers.okta", |
|
|
|
"dj_rest_auth", |
|
|
|
"dj_rest_auth.registration", |
|
|
|
"django.contrib.sites", |
|
|
|
"django_celery_results", |
|
|
|
"django_drf_filepond", |
|
|
|
"health_check", |
|
|
@ -280,3 +282,11 @@ CELERY_TASK_SERIALIZER = "json" |
|
|
|
CELERY_RESULT_SERIALIZER = "json" |
|
|
|
|
|
|
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" |
|
|
|
|
|
|
|
SOCIALACCOUNT_PROVIDERS = { |
|
|
|
"okta": { |
|
|
|
"OKTA_BASE_URL": env("OAUTH_OKTA_OAUTH2_API_URL", ""), |
|
|
|
"OAUTH_PKCE_ENABLED": True, |
|
|
|
"APP": {"client_id": env("OAUTH_OKTA_OAUTH2_KEY", ""), "secret": env("OAUTH_OKTA_OAUTH2_SECRET", "")}, |
|
|
|
} |
|
|
|
} |
|
|
@ -52,6 +52,8 @@ if settings.DEBUG or os.environ.get("STANDALONE", False): |
|
|
|
urlpatterns += [ |
|
|
|
path("admin/", admin.site.urls), |
|
|
|
path("api-auth/", include("rest_framework.urls")), |
|
|
|
path("social/", include("social.urls")), |
|
|
|
path("v1/social/", include("social.v1_urls")), |
|
|
|
path("v1/health/", include("health_check.urls")), |
|
|
|
path("v1/", include("api.urls")), |
|
|
|
path("v1/", include("roles.urls")), |
|
|
|
|
|
@ -126,7 +126,8 @@ known_first_party = [ |
|
|
|
"metrics", |
|
|
|
"projects", |
|
|
|
"roles", |
|
|
|
"users" |
|
|
|
"users", |
|
|
|
"social" |
|
|
|
] |
|
|
|
known_local_folder = [ |
|
|
|
"api", |
|
|
@ -140,7 +141,8 @@ known_local_folder = [ |
|
|
|
"metrics", |
|
|
|
"projects", |
|
|
|
"roles", |
|
|
|
"users" |
|
|
|
"users", |
|
|
|
"social" |
|
|
|
] |
|
|
|
|
|
|
|
[tool.taskipy.tasks] |
|
|
|
|
|
@ -0,0 +1,6 @@ |
|
|
|
from django.apps import AppConfig |
|
|
|
|
|
|
|
|
|
|
|
class SocialConfig(AppConfig): |
|
|
|
default_auto_field = "django.db.models.BigAutoField" |
|
|
|
name = "social" |
|
|
@ -0,0 +1,9 @@ |
|
|
|
from allauth.socialaccount.providers.oauth2.client import OAuth2Client |
|
|
|
from allauth.socialaccount.providers.okta.views import OktaOAuth2Adapter |
|
|
|
from dj_rest_auth.registration.views import SocialLoginView |
|
|
|
|
|
|
|
|
|
|
|
class OktaLogin(SocialLoginView): |
|
|
|
adapter_class = OktaOAuth2Adapter |
|
|
|
callback_url = "/projects" |
|
|
|
client_class = OAuth2Client |
|
|
@ -0,0 +1,7 @@ |
|
|
|
from django.urls import path |
|
|
|
|
|
|
|
from .okta import OktaLogin |
|
|
|
|
|
|
|
urlpatterns = [ |
|
|
|
path("complete/okta-oauth2/", OktaLogin.as_view(), name="okta_login"), |
|
|
|
] |
|
|
@ -0,0 +1,7 @@ |
|
|
|
from django.urls import path |
|
|
|
|
|
|
|
from .views import Social |
|
|
|
|
|
|
|
urlpatterns = [ |
|
|
|
path("links/", Social.as_view()), |
|
|
|
] |
|
|
@ -0,0 +1,26 @@ |
|
|
|
from django.conf import settings |
|
|
|
from rest_framework.response import Response |
|
|
|
from rest_framework.views import APIView |
|
|
|
|
|
|
|
|
|
|
|
class Social(APIView): |
|
|
|
permission_classes = () |
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs): |
|
|
|
return Response( |
|
|
|
{ |
|
|
|
"okta": { |
|
|
|
"type": "oauth2", |
|
|
|
"base_url": settings.SOCIALACCOUNT_PROVIDERS.get("okta").get("OKTA_BASE_URL"), |
|
|
|
"client_id": settings.SOCIALACCOUNT_PROVIDERS.get("okta").get("APP").get("client_id"), |
|
|
|
"redirect_path": "/social/complete/okta-oauth2", |
|
|
|
"authorize_url": "https://" |
|
|
|
+ settings.SOCIALACCOUNT_PROVIDERS.get("okta").get("OKTA_BASE_URL") |
|
|
|
+ "/oauth2/v1/authorize?response_type=code&client_id=" |
|
|
|
+ settings.SOCIALACCOUNT_PROVIDERS.get("okta").get("APP").get("client_id") |
|
|
|
+ "&scope=openid&state=unknown&response_mode=form_post", |
|
|
|
} |
|
|
|
if settings.SOCIALACCOUNT_PROVIDERS.get("okta").get("OKTA_BASE_URL") |
|
|
|
else {}, |
|
|
|
} |
|
|
|
) |
|
|
@ -0,0 +1,46 @@ |
|
|
|
<template> |
|
|
|
<div> |
|
|
|
<v-btn |
|
|
|
v-for="item in social" |
|
|
|
:key="item.provider" |
|
|
|
block |
|
|
|
elevation="2" |
|
|
|
color="secondary" |
|
|
|
:href="item.href" |
|
|
|
class="mt-5" |
|
|
|
> |
|
|
|
{{ $t('user.socialLogin', { provider: item.provider }) }} |
|
|
|
</v-btn> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script lang="ts"> |
|
|
|
import Vue from 'vue' |
|
|
|
|
|
|
|
export default Vue.extend({ |
|
|
|
props: { |
|
|
|
fetchSocialLink: { |
|
|
|
type: Function, |
|
|
|
default: () => Promise |
|
|
|
} |
|
|
|
}, |
|
|
|
data() { |
|
|
|
return { |
|
|
|
social: {} |
|
|
|
} |
|
|
|
}, |
|
|
|
async mounted() { |
|
|
|
const response = await this.fetchSocialLink() |
|
|
|
this.social = Object.entries(response) |
|
|
|
.map(([key, value]: any) => ({ |
|
|
|
provider: key, |
|
|
|
value |
|
|
|
})) |
|
|
|
.filter((item) => !!item.value?.authorize_url) |
|
|
|
.map((item: any) => ({ |
|
|
|
...item, |
|
|
|
href: `${item.value.authorize_url}&redirect_uri=${location.origin}${item.value.redirect_path}` |
|
|
|
})) |
|
|
|
} |
|
|
|
}) |
|
|
|
</script> |
|
|
@ -2,4 +2,6 @@ export interface AuthRepository { |
|
|
|
login(username: string, password: string): Promise<void> |
|
|
|
|
|
|
|
logout(): Promise<void> |
|
|
|
|
|
|
|
socialLink(): Promise<any[]> |
|
|
|
} |
|
|
@ -2,5 +2,6 @@ export default { |
|
|
|
login: 'Einloggen', |
|
|
|
signOut: 'Ausloggen', |
|
|
|
username: 'Benutzername', |
|
|
|
password: 'Passwort' |
|
|
|
password: 'Passwort', |
|
|
|
socialLogin: 'Anmeldung über {provider}' |
|
|
|
} |
|
|
@ -2,5 +2,6 @@ export default { |
|
|
|
login: 'Login', |
|
|
|
signOut: 'Sign Out', |
|
|
|
username: 'Username', |
|
|
|
password: 'Password' |
|
|
|
password: 'Password', |
|
|
|
socialLogin: 'Login With {provider}' |
|
|
|
} |
|
|
@ -2,5 +2,6 @@ export default { |
|
|
|
login: 'Connexion', |
|
|
|
signOut: 'Déconnexion', |
|
|
|
username: "Nom d'utilisateur", |
|
|
|
password: 'Mot de passe' |
|
|
|
password: 'Mot de passe', |
|
|
|
socialLogin: 'Connexion via {provider}' |
|
|
|
} |
|
|
@ -2,5 +2,6 @@ export default { |
|
|
|
login: '登录', |
|
|
|
signOut: '注销', |
|
|
|
username: '用户名', |
|
|
|
password: '密码' |
|
|
|
password: '密码', |
|
|
|
socialLogin: '通过 {provider} 登录' |
|
|
|
} |
|
|
@ -5,6 +5,8 @@ |
|
|
|
<v-row align="center" justify="center"> |
|
|
|
<v-col cols="12" sm="8" md="4"> |
|
|
|
<form-login :login="authenticateUser" /> |
|
|
|
|
|
|
|
<social-login :fetch-social-link="fetchSocialLink" /> |
|
|
|
</v-col> |
|
|
|
</v-row> |
|
|
|
</v-container> |
|
|
@ -16,14 +18,16 @@ |
|
|
|
import Vue from 'vue' |
|
|
|
import { mapActions } from 'vuex' |
|
|
|
import FormLogin from '@/components/auth/FormLogin.vue' |
|
|
|
import SocialLogin from '@/components/auth/SocialLogin.vue' |
|
|
|
|
|
|
|
export default Vue.extend({ |
|
|
|
components: { |
|
|
|
FormLogin |
|
|
|
FormLogin, |
|
|
|
SocialLogin |
|
|
|
}, |
|
|
|
|
|
|
|
methods: { |
|
|
|
...mapActions('auth', ['authenticateUser']) |
|
|
|
...mapActions('auth', ['authenticateUser', 'fetchSocialLink']) |
|
|
|
} |
|
|
|
}) |
|
|
|
</script> |
|
|
@ -13,4 +13,10 @@ export class APIAuthRepository implements AuthRepository { |
|
|
|
const url = '/auth/logout/' |
|
|
|
await this.request.post(url) |
|
|
|
} |
|
|
|
|
|
|
|
async socialLink(): Promise<any[]> { |
|
|
|
const url = '/social/links/' |
|
|
|
const response = await this.request.get(url) |
|
|
|
return response.data |
|
|
|
} |
|
|
|
} |
|
|
@ -10,4 +10,8 @@ export class AuthApplicationService { |
|
|
|
public async logout() { |
|
|
|
await this.repository.logout() |
|
|
|
} |
|
|
|
|
|
|
|
public async socialLink() { |
|
|
|
return await this.repository.socialLink() |
|
|
|
} |
|
|
|
} |
|
|
@ -47,6 +47,9 @@ export const actions = { |
|
|
|
throw new Error('The credential is invalid') |
|
|
|
} |
|
|
|
}, |
|
|
|
async fetchSocialLink() { |
|
|
|
return await this.$services.auth.socialLink() |
|
|
|
}, |
|
|
|
async initAuth({ commit }) { |
|
|
|
try { |
|
|
|
const user = await this.$services.user.getMyProfile() |
|
|
|