If you can model the authentication process as a state machine, have a pure function which accepts auth-state and data and returns either a new state or an operation to get new data to determine the next state.
E.g. (next-auth-state {:stage :not-authed :login "login" :password "pass"} nil) => {:state {:stage :not-authed ...} :operations [{:kind :get-auth-token :args {:login "login" ...}]}}. Then (next-auth-state {:stage :not-authed} {:auth-token ""}) => {state {:stage :has-token :token "..." :expires "..."} :operations [{:kind :get-session-token :args {:session-token "token"}{:kind :invalidate-token :args {:session-token "token"}}] etc. In state machine graph terms, the :stage is the node you are on, the :operations are actions which you can take to return data which represent the edges the state machine can follow. Then you have a higher-level reduction function which accepts any auth state map and keeps running the state machine until it can return a map that is suitable for authentication. This function is responsible (possibly indirectly) for turning operation requests into http calls and collecting the responses, i.e. is impure, but not necessarily stateful or mutable. E.g. auth-state map could start in {:stage :not-authed :login "login" :password "pass"}. (authorize auth-state) is a reduction function that runs (get-auth-token auth-state), returns {:stage :token :expires #inst"..."}, then authorize inspects return value and runs (open-session {:stage :token :expires "..."}) etc, until either a failure or success. Lower-level client api calls receive only auth-maps with a non-expired stage=:session. They must return, in addition to success or failure of the call itself, any auth-related state info from the server which should alter the auth-map somehow. Another function (maybe authorize, maybe something else) integrates this state with the passed-in auth map and returns a new (possibly unchanged) auth-map, which can be used for future calls. Higher-level client api functions should combine api calls with auth-state updating. If you keep your fn argument and return structure regular enough, you might even be able to do this with a single higher-order function: (call-with-auth low-level-client-api-fn auth-map args) -> {:result x :auth-map new-auth-map-state} Only at this point might it be convenient to have a higher-level, stateful construct which completely hides the auth-state map from you. e.g. (make-session login pass) => {:auth-state (atom {:state :not-authed :login login :pass pass})}, then (call-with-session session client-fn args) => result-only, and mutates the auth state in the atom for you. But the emphasis here is on programmer convenience (not having to collect the new-auth-state return value and propagate it around), not any necessity of modeling the auth flow. You still need to think about concurrency at the communication level. If two client api calls run at the same time, both with expired tokens, what can happen? Can both independently run the update with separate http calls and get different tokens (or will the server issue the same token to both)? Should only one http communication happen on the client side and the client is responsible for blocking callers that want a new token until a single token is reissued? Your decision affects the implementation of the state-machine driving function (the one that executes operations and calls next-state repeatedly): you might put a token and session cache in there, or put operations related to the same login on their own queue so there is only a single writer per login, or whatever. But the hard auth flow logic in your next-state function remains the same. On, Friday, April 15, 2016 at 7:40:48 AM UTC-5, Stefan Kamphausen wrote: > > Hi, > > > Currently, I am in the process of writing a client to server API which is > not trivial to consume. In particular it needs a 3-step authentication > process: login with user name and password, get an authentication token, > open a session with the token and finally consume the API with the > session. Sessions and tokens can expire and the client should handle that > transparently: if it has a token, create a new session, if the token > expired and it has username and password, create a new token... So, > sessions and tokens would have to be local, mutable, encapsulated state, as > far as I can see. > > Now; I wonder how to best model this in Clojure. > > My favorite right now is, creating a closure over a local atom and return > it to the user. The downside to this is that it feels unnatural to consume > different parts of the API, e.g. (client :do-something &args) vs (client > :do-something-else & other-args). It would be nice to defined a protocol > with function do-something and do-something-else but then I would have to > pass the atom as an argument to the record which feels even worse. > > Am I missing an obvious other solution? Have you done something similar, > how? > > Any pointers welcome. > > > Best regards, > stefan > -- You received this message because you are subscribed to the Google Groups "Clojure" group. To post to this group, send email to clojure@googlegroups.com Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to clojure+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/clojure?hl=en --- You received this message because you are subscribed to the Google Groups "Clojure" group. To unsubscribe from this group and stop receiving emails from it, send an email to clojure+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.