diff --git a/client/src/App.js b/client/src/App.js
index 2000f5b1..a353fdf9 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -7,8 +7,10 @@ import Main from './containers/Main';
import { theme, MuiThemeProvider } from './components/elements';
export default () => (
-
-
+
+
@@ -18,6 +20,6 @@ export default () => (
-
-
+
+
);
diff --git a/client/src/components/elements/SpeciesSelect.js b/client/src/components/elements/SpeciesSelect.js
index b44c8a31..88e33df1 100644
--- a/client/src/components/elements/SpeciesSelect.js
+++ b/client/src/components/elements/SpeciesSelect.js
@@ -7,7 +7,7 @@ import { useDataFetch } from '../../containers/Authenticate';
const SpeciesSelect = (props) => {
const memoizedFetchFunc = useCallback(
- (authorizedFetch) =>
+ (fetchFn) =>
mockFetchOrNot(
(mockFetch) => {
return mockFetch.get('*', [
@@ -27,7 +27,7 @@ const SpeciesSelect = (props) => {
]);
},
() =>
- authorizedFetch(`/api/species`, {
+ fetchFn(`/api/species`, {
method: 'GET',
})
),
diff --git a/client/src/containers/Authenticate/Profile.js b/client/src/containers/Authenticate/Profile.js
index 37f57f6f..ca3b8deb 100644
--- a/client/src/containers/Authenticate/Profile.js
+++ b/client/src/containers/Authenticate/Profile.js
@@ -19,7 +19,6 @@ Profile.propTypes = {
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
- onLogout: PropTypes.func.isRequired,
children: PropTypes.element,
};
diff --git a/client/src/containers/Authenticate/TokenMgmt.js b/client/src/containers/Authenticate/TokenMgmt.js
index 9f648412..4c856b9f 100644
--- a/client/src/containers/Authenticate/TokenMgmt.js
+++ b/client/src/containers/Authenticate/TokenMgmt.js
@@ -1,50 +1,114 @@
-import React, { useContext, useReducer } from 'react';
-import { Button } from '../../components/elements';
+import React, { useContext, useReducer, useEffect } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
+import { Button } from '../../components/elements';
import AuthorizationContext from '../../containers/Authenticate/AuthorizationContext';
+import { useCallback } from 'react';
+
+const ACTION_STORE = 'STORE';
+const ACTION_REVOKE = 'REVOKE';
+const UPDATE_METADATA = 'UPDATE_METADATA';
+
+function tokenMetaDataReducer(state, action) {
+ let newState = { ...state };
+
+ switch (action.type) {
+ case UPDATE_METADATA:
+ console.log('Metadata update trigerred.');
+ newState = { ...action['payload'] };
+ break;
+ default:
+ console.log('Invalid action type detected:');
+ console.log(action.type);
+ throw new Error();
+ }
-function TokenMgmt() {
- const ACTION_STORE = 'STORE';
- const ACTION_REVOKE = 'REVOKE';
+ return newState;
+}
+
+function tokenReducer(state, action) {
+ const newState = { ...state };
+
+ switch (action.type) {
+ case ACTION_STORE:
+ newState['apiToken'] = action.payload;
+ break;
+ case ACTION_REVOKE:
+ newState['apiToken'] = null;
+ break;
+ default:
+ console.log('Invalid action type detected:');
+ console.log(action.type);
+ throw new Error();
+ }
+ return newState;
+}
+
+function TokenMgmt() {
const { authorizedFetch, user } = useContext(AuthorizationContext);
const [tokenState, dispatchTokenState] = useReducer(tokenReducer, {
apiToken: null,
});
- const defaultTokenInstructions =
- 'No stored ID token to display.\n' +
- "Click the 'Store token' button below to store the current ID token and display it here.";
-
- function tokenReducer(state, action) {
- const newState = { ...state };
-
- switch (action.type) {
- case ACTION_STORE:
- newState['apiToken'] = user.id_token;
- break;
- case ACTION_REVOKE:
- newState['apiToken'] = null;
- break;
- default:
- console.log('Invalid action type detected:');
- console.log(action.type);
- throw new Error();
+ const [tokenMetaDataState, dispatchTokenMetaData] = useReducer(
+ tokenMetaDataReducer,
+ {
+ 'token-stored?': false,
+ 'last-used': null,
}
+ );
- return newState;
- }
+ const updateTokenMetadata = useCallback(
+ () => {
+ authorizedFetch('/api/auth/token-metadata', { method: 'GET' })
+ .then((response) => {
+ if (response.ok) {
+ return response.json();
+ } else {
+ console.log(
+ 'Error while retrieving token metadata. Returned response: ',
+ response
+ );
+ throw new Error('Error while retrieving token metadata');
+ }
+ })
+ .then((data) => {
+ console.log('token-metadata result received:', data);
+
+ dispatchTokenMetaData({ type: UPDATE_METADATA, payload: data });
+ })
+ .catch((error) => {
+ console.log(
+ 'Error caught on authorizedFetch for token-metadata:',
+ error
+ );
+ });
+ },
+ [authorizedFetch]
+ );
+
+ const noTokenInstructions =
+ 'No stored ID token to display.\n' +
+ "Click the 'Store token' button below to store the current ID token and display it here.";
+ const newTokenInstructions =
+ 'Stored tokens can not be retrieved for display after storage.\n' +
+ "Click the 'Store token' button below to store a new token (invalidating the current stored token) and display it here.";
function storeTokenHandler() {
console.log('storeTokenHandler triggered.');
authorizedFetch(`/api/auth/token`, {
method: 'POST',
+ }).then((response) => {
+ if (response.ok) {
+ dispatchTokenState({ type: ACTION_STORE, payload: user.id_token });
+ } else {
+ console.log('Error returned by /auth/token POST endpoint.');
+ throw new Error('API endpoint for token storage returned error.');
+ }
});
-
- dispatchTokenState({ type: ACTION_STORE });
}
function revokeTokenHandler() {
@@ -52,17 +116,41 @@ function TokenMgmt() {
authorizedFetch(`/api/auth/token`, {
method: 'DELETE',
+ }).then((response) => {
+ if (response.ok) {
+ dispatchTokenState({ type: ACTION_REVOKE });
+ } else {
+ console.log('Error returned by /auth/token DELETE endpoint.');
+ throw new Error('API endpoint for token revoking returned error.');
+ }
});
-
- dispatchTokenState({ type: ACTION_REVOKE });
}
+ useEffect(
+ () => {
+ updateTokenMetadata();
+ },
+ [tokenState, updateTokenMetadata]
+ );
+
return (
+
+ Token stored?: {tokenMetaDataState['token-stored?'] ? 'Yes' : 'No'}
+
+
+
Token last used: {tokenMetaDataState['last-used'] || 'Never'}
+
diff --git a/client/src/containers/Main/index.js b/client/src/containers/Main/index.js
index 8b0f4ba3..b059694c 100644
--- a/client/src/containers/Main/index.js
+++ b/client/src/containers/Main/index.js
@@ -65,9 +65,11 @@ function Main({ classes }) {
component={() => (
-
-
-
+ <>
+
+
+
+ >
)}
diff --git a/resources/schema/definitions.edn b/resources/schema/definitions.edn
index 6759711d..ce329c39 100644
--- a/resources/schema/definitions.edn
+++ b/resources/schema/definitions.edn
@@ -106,6 +106,18 @@
:cardinality :db.cardinality/one
:unique :db.unique/value
:noHistory true}
+ #:db{:ident :person/auth-token-stored-at
+ :valueType :db.type/instant
+ :cardinality :db.cardinality/one
+ :doc "When the current auth-token was stored."}
+ #:db{:ident :person/auth-token-last-used
+ :valueType :db.type/instant
+ :cardinality :db.cardinality/one
+ :doc "When the stored auth-token was last used to access the API."}
+ #:db{:ident :person/last-activity
+ :valueType :db.type/instant
+ :cardinality :db.cardinality/one
+ :doc "When the user last showed any activity in the NS (through either API or web)."}
#:db{:ident :person/name
:valueType :db.type/string
:cardinality :db.cardinality/one
diff --git a/src/wormbase/names/auth.clj b/src/wormbase/names/auth.clj
index 308573ce..e753c578 100644
--- a/src/wormbase/names/auth.clj
+++ b/src/wormbase/names/auth.clj
@@ -13,7 +13,8 @@
[ring.middleware.defaults :as rmd]
[ring.util.http-response :as http-response]
[wormbase.specs.auth :as ws-auth]
- [wormbase.names.util :as wnu])
+ [wormbase.names.util :as wnu]
+ [wormbase.util :as wu])
(:import (com.google.api.client.auth.oauth2 TokenResponseException)
(com.google.api.client.googleapis.auth.oauth2 GoogleAuthorizationCodeTokenRequest GoogleIdToken GoogleIdTokenVerifier$Builder)
(com.google.api.client.http.javanet NetHttpTransport)
@@ -108,12 +109,12 @@
(log/warn "Token failed google API verification.")))
(defn query-person
- "Query the database for a WormBase person, given the schema attribute
- and token associated with authentication.
+ "Query the database for a WormBase person, given a unique schema attribute
+ and a unique value to match.
Return a map containing the information about a person, omitting the auth token."
- [db ident auth-token]
- (let [person (d/pull db '[*] [ident auth-token])]
+ [db ident-attr ident-value]
+ (let [person (d/pull db '[*] [ident-attr ident-value])]
(when (:db/id person)
(dissoc person :person/auth-token))))
@@ -141,19 +142,38 @@
(defrecord Identification [id-token token-info person])
-(defn get-verified-person
+(defn get-person-matching-token
"Returns a person from the database with:
* Matching email address
* Active profile state in DB
* A matching stored (signed) auth-token"
- [db token-str email]
+ [conn db token-str email]
(when-let [person (d/pull db
'[:person/auth-token :person/active?]
[:person/email email])]
(when (and (:person/active? person)
(matching-token-hash? token-str (:person/auth-token person)))
- (query-person db :person/email email))))
+ ;;Update :person/auth-token-last-used attribute to indicate API token usage
+ (let [person (query-person db :person/email email)]
+ @(d/transact conn
+ [[:db/add
+ (:db/id person)
+ :person/auth-token-last-used
+ (wu/now)]])
+ person))))
+
+(defn successful-login
+ "Store the identification of a successful login
+ and store the login timestamp in the DB."
+ [conn identification]
+ (let [person (:person identification)]
+ @(d/transact conn
+ [[:db/add
+ (:db/id person)
+ :person/last-activity
+ (wu/now)]]))
+ (map->Identification identification))
(defn identify
"Identify the wormbase user associated with request based on token.
@@ -167,14 +187,15 @@
parsed-token-map (get-token-payload-map google-ID-token-str)
email (some-> parsed-token-map
(:email parsed-token-map))
- db (:db request)]
+ db (:db request)
+ conn (:conn request)]
(log/debug "Initiating token-based identification.")
(if parsed-token-map
- (if-let [person (get-verified-person db google-ID-token-str email)]
+ (if-let [person (get-person-matching-token conn db google-ID-token-str email)]
;Verified person found matching token
(do
- (log/debug "Verified person found on matching stored auth-code:" person)
- (map->Identification {:id-token google-ID-token-str :token-info parsed-token-map :person person}))
+ (log/debug "Verified person found with matching stored auth-code:" person)
+ (successful-login conn {:id-token google-ID-token-str :token-info parsed-token-map :person person}))
;No verified person found matching token
(if-let [tok (verify-token google-ID-token-str)]
;Provided token verified
@@ -183,7 +204,7 @@
(do
(log/debug "No verified person found based on stored auth-code,
but token verified as valid. Person matching verified token: " person)
- (map->Identification {:id-token google-ID-token-str :token-info parsed-token-map :person person}))
+ (successful-login conn {:id-token google-ID-token-str :token-info parsed-token-map :person person}))
;No person found matching verified token
(log/warn "No person found in db for verified token:" tok))
;Provided token fails to verify
@@ -234,29 +255,50 @@
(defn store-auth-token [request]
(if-let [signed-auth-token (some->
(wnu/unqualify-keys (-> request :identity) "identity")
- (:id-token identity)
+ (:id-token)
(derive-token-hash))]
(let [person (-> (wnu/unqualify-keys (-> request :identity) "identity")
- (:person identity))]
+ (:person))]
(log/debug "Storing new auth-token for user" (:person/email person))
@(d/transact (:conn request)
[[:db/add
(:db/id person)
:person/auth-token
- signed-auth-token]])
+ signed-auth-token]
+ [:db/add
+ (:db/id person)
+ :person/auth-token-stored-at
+ (wu/now)]])
(http-response/ok))
(http-response/internal-server-error)))
(defn delete-auth-token [request]
(let [person (-> (wnu/unqualify-keys (-> request :identity) "identity")
- (:person identity))]
+ (:person))]
(log/debug "Revoking stored auth-token for user" (:person/email person))
@(d/transact (:conn request)
[[:db/retract
(:db/id person)
- :person/auth-token]])
+ :person/auth-token]
+ [:db/retract
+ (:db/id person)
+ :person/auth-token-stored-at]
+ [:db/retract
+ (:db/id person)
+ :person/auth-token-last-used]])
(http-response/ok)))
+(defn get-token-metadata [request]
+ (let [person (-> (wnu/unqualify-keys (-> request :identity) "identity")
+ (:person)
+ (wnu/unqualify-keys "person"))
+ token-last-used (:auth-token-last-used person)
+ _ (log/debug "token-last-used:" token-last-used)
+ token-stored? (not (nil? (:auth-token-stored-at person)))
+ _ (log/debug "token-stored?:" token-stored?)]
+ (http-response/ok {:token-stored? token-stored?
+ :last-used token-last-used})))
+
;; API endpoints
(def routes
(sweet/routes
@@ -271,7 +313,7 @@
:responses (wnu/http-responses-for-read {:schema ::ws-auth/identity-response})
:handler get-identity}}))
(sweet/context "/token" []
- :tags ["authenticate"]
+ :tags ["authentication"]
(sweet/resource
{:post
{:summary "Store the current ID token for future scripting usage. This will invalidate the previously stored token."
@@ -281,4 +323,12 @@
{:summary "Delete the stored token, invalidating it for future use."
:responses (wnu/response-map http-response/ok {:schema ::ws-auth/empty-response})
:handler delete-auth-token}}))
+ (sweet/context "/token-metadata" []
+ :tags ["authentication"]
+ (sweet/resource
+ {:get
+ {:summary "Get token metadata such as token storage state (yes/no) and usage."
+ :x-name ::get-token-metadata
+ :responses (wnu/http-responses-for-read {:schema ::ws-auth/token-metadata-response})
+ :handler get-token-metadata}}))
)))
diff --git a/src/wormbase/specs/auth.clj b/src/wormbase/specs/auth.clj
index 02725d58..7765d8f7 100644
--- a/src/wormbase/specs/auth.clj
+++ b/src/wormbase/specs/auth.clj
@@ -36,4 +36,18 @@
(s/def ::identity-response (stc/spec {:spec (s/keys :req-un [::id-token ::token-info ::person])
:swagger/example example-identity}))
+(s/def ::token-stored? (stc/spec {:spec sts/boolean?
+ :swagger/example false
+ :description "Boolean indicating whether or not a token is currently stored for a person."}))
+
+(s/def ::last-used (stc/spec {:spec (s/nilable sts/inst?)
+ :swagger/example "2023-12-19T14:11:34Z"
+ :description "Datestring indicating when a token was last used to access the name service."}))
+
+(def example-token-metadata {:token-stored? true
+ :last-used "2023-12-19T14:11:34Z"})
+
+(s/def ::token-metadata-response (stc/spec {:spec (s/keys :req-un [::token-stored? ::last-used])
+ :swagger/example example-token-metadata}))
+
(s/def ::empty-response (stc/spec {:spec (s/cat)}))