Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions specifyweb/backend/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']))
Expand Down Expand Up @@ -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'])
Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions specifyweb/frontend/js_src/lib/components/Login/OicLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -37,6 +39,24 @@ export function OicLogin({
const providerRef = React.useRef<HTMLInputElement | null>(null);
const formRef = React.useRef<HTMLFormElement | null>(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 }) => (
<ErrorDialog
copiableMessage={message}
dismissible
header={message}
onClose={handleClose}
>
{message}
</ErrorDialog>
));
}, [inviteExpired, setInviteExpired]);

return (
<SplashScreen>
<LoginLanguageChooser languages={data.languages} />
Expand Down
3 changes: 3 additions & 0 deletions specifyweb/frontend/js_src/lib/localization/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Loading