diff --git a/specifyweb/backend/accounts/views.py b/specifyweb/backend/accounts/views.py index b2dc0358d35..90d8ee8bc36 100644 --- a/specifyweb/backend/accounts/views.py +++ b/specifyweb/backend/accounts/views.py @@ -156,19 +156,18 @@ def oic_callback(request: http.HttpRequest) -> http.HttpResponse: if 'invite_token' in request.session: # We are in the invite link workflow. token: InviteToken = request.session['invite_token'] + del request.session['invite_token'] user = Specifyuser.objects.annotate(token_seq=Max('spuserexternalid__id')).get(id=token['userid']) - if time.time() > token['expires'] or user.token_seq != token['sequence']: - return http.HttpResponseBadRequest("Token expired.", content_type="text/plain") - - user.spuserexternalid_set.create( - provider=provider, - providerid=str(ext_user['sub']), - idtoken=ext_user, - ) - del request.session['invite_token'] - login(request, user, backend='django.contrib.auth.backends.ModelBackend') - return http.HttpResponseRedirect('/accounts/choose_collection/') + if time.time() <= token['expires'] and user.token_seq == token['sequence']: + user.spuserexternalid_set.create( + provider=provider, + providerid=str(ext_user['sub']), + idtoken=ext_user, + ) + login(request, user, backend='django.contrib.auth.backends.ModelBackend') + return http.HttpResponseRedirect('/accounts/choose_collection/') + # Invite already used or expired: fall through to normal SSO login. try: spuserexternalid = Spuserexternalid.objects.get(provider=provider, providerid=str(ext_user['sub'])) @@ -228,6 +227,9 @@ def use_invite_link(request) -> http.HttpResponse: it in a server side session variable, then redirect to the regular OIC login process which will retrieve the token after the user authenticates with their chosen IdP. + + Expired or already-used invite links redirect to the normal login + page so users who bookmark/reuse the invite URL can still sign in. """ message = request.GET['token'].encode('utf-8') mac = base64.urlsafe_b64decode(request.GET['mac']) @@ -239,7 +241,10 @@ def use_invite_link(request) -> http.HttpResponse: user = Specifyuser.objects.annotate(token_seq=Max('spuserexternalid__id')).get(id=token['userid']) if time.time() > token['expires'] or user.token_seq != token['sequence']: - return http.HttpResponseBadRequest("Token expired.", content_type="text/plain") + # Do not clear an existing session invite_token here: the user may + # still have a valid invite in progress from a newer link. + # invite_expired=1 lets the login UI show a non-blocking notice. + return http.HttpResponseRedirect('/accounts/login/?invite_expired=1') token['username'] = user.name # Just in case it has changed. request.session['invite_token'] = token diff --git a/specifyweb/frontend/js_src/lib/components/Login/OicLogin.tsx b/specifyweb/frontend/js_src/lib/components/Login/OicLogin.tsx index 6c598cadd04..7144bce8054 100644 --- a/specifyweb/frontend/js_src/lib/components/Login/OicLogin.tsx +++ b/specifyweb/frontend/js_src/lib/components/Login/OicLogin.tsx @@ -13,7 +13,9 @@ import { Button } from '../Atoms/Button'; import { Form } from '../Atoms/Form'; import { Link } from '../Atoms/Link'; import { Submit } from '../Atoms/Submit'; +import { displayError } from '../Core/Contexts'; import { SplashScreen } from '../Core/SplashScreen'; +import { ErrorDialog } from '../Errors/ErrorDialog'; import { formatUrl } from '../Router/queryString'; import { LoginLanguageChooser } from './index'; @@ -37,6 +39,24 @@ export function OicLogin({ const providerRef = React.useRef(null); const formRef = React.useRef(null); const [next = ''] = useSearchParameter('next'); + const [inviteExpired, setInviteExpired] = useSearchParameter('invite_expired'); + + React.useEffect(() => { + if (inviteExpired !== '1') return; + setInviteExpired(undefined); + const message = userText.inviteLinkExpired(); + displayError(({ onClose: handleClose }) => ( + + {message} + + )); + }, [inviteExpired, setInviteExpired]); + return ( diff --git a/specifyweb/frontend/js_src/lib/localization/user.ts b/specifyweb/frontend/js_src/lib/localization/user.ts index 9a529a8f3ca..f1cd5f9c24d 100644 --- a/specifyweb/frontend/js_src/lib/localization/user.ts +++ b/specifyweb/frontend/js_src/lib/localization/user.ts @@ -178,6 +178,9 @@ export const userText = createDictionary({ 'hr-hr': 'Pozdrav, {userName:string}!', nb: 'Hallo, {userName:string}!', }, + inviteLinkExpired: { + 'en-us': 'This invite link has already been used or has expired.', + }, oicWelcomeMessage: { 'en-us': "You've been invited to associate an external login to your Specify user account. This will enable you to log in to Specify with your chosen provider going forward.",