adam@26
|
1 style provider
|
adam@26
|
2
|
adam@26
|
3 style aol
|
adam@26
|
4 style google
|
adam@26
|
5 style myspace
|
adam@26
|
6 style yahoo
|
adam@26
|
7
|
adam@28
|
8 datatype choose_result a = Success of a | Failure of string
|
adam@28
|
9
|
adam@16
|
10 functor Make(M: sig
|
adam@16
|
11 con cols :: {Type}
|
adam@16
|
12 constraint [Id] ~ cols
|
adam@17
|
13 val folder : folder cols
|
adam@17
|
14 val inj : $(map sql_injectable cols)
|
adam@17
|
15
|
adam@17
|
16 type creationState
|
adam@17
|
17 type creationData
|
adam@17
|
18 val creationState : transaction creationState
|
adam@17
|
19 val render : creationState -> xtable
|
adam@20
|
20 val ready : creationState -> signal bool
|
adam@17
|
21 val tabulate : creationState -> signal creationData
|
adam@28
|
22 val choose : sql_table ([Id = string] ++ cols) [Pkey = [Id]] -> creationData -> transaction (choose_result $cols)
|
adam@16
|
23
|
adam@16
|
24 val sessionLifetime : int
|
adam@16
|
25 val afterLogout : url
|
adam@16
|
26 val secureCookies : bool
|
adam@16
|
27 val association : Openid.association_mode
|
adam@16
|
28 val realm : option string
|
adam@17
|
29 val formClass : css_class
|
adam@23
|
30 val fakeId : option string
|
kkallio@31
|
31
|
adam@32
|
32 val ctlDisplay : {User : {Status : xbody, Logout : xbody},
|
adam@32
|
33 Guest : {Status : xbody, Signup : xbody}}
|
adam@16
|
34 end) = struct
|
adam@16
|
35
|
adam@16
|
36 type user = string
|
adam@28
|
37 val eq_user = _
|
adam@28
|
38 val read_user = _
|
adam@16
|
39 val show_user = _
|
adam@16
|
40 val inj_user = _
|
adam@16
|
41
|
adam@16
|
42 table user : ([Id = user] ++ M.cols)
|
adam@16
|
43 PRIMARY KEY Id
|
adam@16
|
44
|
adam@16
|
45 table identity : {User : user, Identifier : string}
|
adam@16
|
46 PRIMARY KEY (User, Identifier)
|
adam@16
|
47
|
adam@16
|
48 sequence sessionIds
|
adam@16
|
49
|
adam@16
|
50 table session : {Id : int, Key : int, Identifier : option string, Expires : time}
|
adam@16
|
51 PRIMARY KEY Id
|
adam@16
|
52
|
adam@22
|
53 datatype authMode =
|
adam@22
|
54 SigningUp of {Session : int, Key : int}
|
adam@22
|
55 | LoggedIn of {User : user, Session : int, Key : int}
|
adam@22
|
56
|
adam@22
|
57 cookie auth : authMode
|
adam@16
|
58
|
adam@17
|
59 val currentUrl =
|
adam@17
|
60 b <- currentUrlHasPost;
|
adam@17
|
61 if b then
|
adam@17
|
62 return M.afterLogout
|
adam@17
|
63 else
|
adam@17
|
64 currentUrl
|
adam@17
|
65
|
adam@16
|
66 val current =
|
adam@22
|
67 login <- getCookie auth;
|
adam@16
|
68 case login of
|
adam@22
|
69 Some (LoggedIn login) =>
|
adam@22
|
70 (ident <- oneOrNoRowsE1 (SELECT (session.Identifier)
|
adam@22
|
71 FROM session
|
adam@22
|
72 WHERE session.Id = {[login.Session]}
|
adam@22
|
73 AND session.Key = {[login.Key]});
|
adam@22
|
74 case ident of
|
adam@22
|
75 None => return None
|
adam@22
|
76 | Some None => return None
|
adam@22
|
77 | Some (Some ident) =>
|
adam@22
|
78 valid <- oneRowE1 (SELECT COUNT( * ) > 0
|
adam@22
|
79 FROM identity
|
adam@22
|
80 WHERE identity.User = {[login.User]}
|
adam@22
|
81 AND identity.Identifier = {[ident]});
|
adam@22
|
82 if valid then
|
adam@22
|
83 return (Some login.User)
|
adam@22
|
84 else
|
adam@22
|
85 error <xml>Session not authorized to act as user</xml>)
|
adam@22
|
86 | _ => return None
|
adam@16
|
87
|
adam@17
|
88 fun validUser s = String.length s > 0 && String.length s < 20
|
adam@17
|
89 && String.all Char.isAlnum s
|
adam@17
|
90
|
adam@16
|
91 fun main wrap =
|
adam@16
|
92 let
|
adam@16
|
93 fun logout () =
|
adam@22
|
94 clearCookie auth;
|
adam@16
|
95 redirect M.afterLogout
|
adam@16
|
96
|
adam@17
|
97 fun signupDetails after =
|
adam@17
|
98 let
|
adam@17
|
99 fun finishSignup uid data =
|
adam@17
|
100 if not (validUser uid) then
|
adam@17
|
101 return (Some "That username is not valid. It must be between 1 and 19 characters long, containing only letters and numbers.")
|
adam@17
|
102 else
|
adam@17
|
103 used <- oneRowE1 (SELECT COUNT( * ) > 0
|
adam@17
|
104 FROM user
|
adam@17
|
105 WHERE user.Id = {[uid]});
|
adam@17
|
106 if used then
|
adam@17
|
107 return (Some "That username is taken. Please choose another.")
|
adam@17
|
108 else
|
adam@22
|
109 ses <- getCookie auth;
|
adam@17
|
110 case ses of
|
adam@17
|
111 None => return (Some "Missing session cookie")
|
adam@22
|
112 | Some (LoggedIn _) => return (Some "Session cookie is for already logged-in user")
|
adam@22
|
113 | Some (SigningUp ses) =>
|
adam@17
|
114 ident <- oneOrNoRowsE1 (SELECT (session.Identifier)
|
adam@17
|
115 FROM session
|
adam@17
|
116 WHERE session.Id = {[ses.Session]}
|
adam@17
|
117 AND session.Key = {[ses.Key]});
|
adam@17
|
118 case ident of
|
adam@17
|
119 None => return (Some "Invalid session data")
|
adam@17
|
120 | Some None => return (Some "Session has no associated identifier")
|
adam@17
|
121 | Some (Some ident) =>
|
adam@28
|
122 cols <- M.choose user data;
|
adam@28
|
123 case cols of
|
adam@28
|
124 Failure s => return (Some s)
|
adam@28
|
125 | Success cols =>
|
adam@28
|
126 setCookie auth {Value = LoggedIn ({User = uid} ++ ses),
|
adam@28
|
127 Expires = None,
|
adam@28
|
128 Secure = M.secureCookies};
|
adam@17
|
129
|
adam@28
|
130 dml (insert user ({Id = (SQL {[uid]})} ++ @Sql.sqexps M.folder M.inj cols));
|
adam@28
|
131 dml (INSERT INTO identity (User, Identifier)
|
adam@28
|
132 VALUES ({[uid]}, {[ident]}));
|
adam@30
|
133 return None
|
adam@17
|
134 in
|
adam@17
|
135 uid <- source "";
|
adam@17
|
136 cs <- M.creationState;
|
adam@17
|
137
|
adam@17
|
138 wrap "Your User Details" <xml>
|
adam@17
|
139 <table class={M.formClass}>
|
adam@17
|
140 <tr> <th class={M.formClass}>Username:</th> <td><ctextbox source={uid}/></td> </tr>
|
adam@17
|
141 {M.render cs}
|
adam@20
|
142 <tr> <td><dyn signal={b <- M.ready cs;
|
adam@20
|
143 return (if b then
|
adam@20
|
144 <xml><button value="Create Account"
|
adam@20
|
145 onclick={uid <- get uid;
|
adam@20
|
146 data <- Basis.current (M.tabulate cs);
|
adam@20
|
147 res <- rpc (finishSignup uid data);
|
adam@20
|
148 case res of
|
adam@20
|
149 None => redirect (bless after)
|
adam@20
|
150 | Some msg => alert msg}/></xml>
|
adam@20
|
151 else
|
adam@20
|
152 <xml/>)}/></td> </tr>
|
adam@17
|
153 </table>
|
adam@17
|
154 </xml>
|
adam@17
|
155 end
|
adam@17
|
156
|
adam@16
|
157 fun opCallback after ses res =
|
adam@16
|
158 case res of
|
adam@16
|
159 Openid.Canceled => error <xml>You canceled the login process.</xml>
|
adam@16
|
160 | Openid.Failure s => error <xml>Login failed: {[s]}</xml>
|
adam@16
|
161 | Openid.AuthenticatedAs ident =>
|
adam@22
|
162 av <- getCookie auth;
|
adam@22
|
163 case av of
|
adam@22
|
164 Some (SigningUp signup) =>
|
adam@16
|
165 if signup.Session <> ses then
|
adam@16
|
166 error <xml>Session has changed suspiciously</xml>
|
adam@16
|
167 else
|
adam@16
|
168 invalid <- oneRowE1 (SELECT COUNT( * ) = 0
|
adam@16
|
169 FROM session
|
adam@16
|
170 WHERE session.Id = {[signup.Session]}
|
adam@16
|
171 AND session.Key = {[signup.Key]});
|
adam@16
|
172 if invalid then
|
adam@16
|
173 error <xml>Invalid or expired session</xml>
|
adam@16
|
174 else
|
adam@17
|
175 dml (UPDATE session
|
adam@17
|
176 SET Identifier = {[Some ident]}
|
adam@17
|
177 WHERE Id = {[signup.Session]});
|
adam@17
|
178 signupDetails after
|
adam@22
|
179 | Some (LoggedIn login) =>
|
adam@22
|
180 if login.Session <> ses then
|
adam@22
|
181 error <xml>Session has changed suspiciously</xml>
|
adam@22
|
182 else
|
adam@22
|
183 invalid <- oneRowE1 (SELECT COUNT( * ) = 0
|
adam@22
|
184 FROM session
|
adam@22
|
185 WHERE session.Id = {[login.Session]}
|
adam@22
|
186 AND session.Key = {[login.Key]});
|
adam@22
|
187 if invalid then
|
adam@22
|
188 error <xml>Invalid or expired session</xml>
|
adam@16
|
189 else
|
adam@22
|
190 dml (UPDATE session
|
adam@22
|
191 SET Identifier = {[Some ident]}
|
adam@22
|
192 WHERE Id = {[login.Session]});
|
adam@22
|
193 redirect (bless after)
|
adam@22
|
194 | None => error <xml>Missing session cookie</xml>
|
adam@16
|
195
|
adam@23
|
196 fun fakeCallback ident after ses =
|
adam@23
|
197 av <- getCookie auth;
|
adam@23
|
198 case av of
|
adam@23
|
199 Some (SigningUp signup) =>
|
adam@23
|
200 invalid <- oneRowE1 (SELECT COUNT( * ) = 0
|
adam@23
|
201 FROM session
|
adam@23
|
202 WHERE session.Id = {[signup.Session]}
|
adam@23
|
203 AND session.Key = {[signup.Key]});
|
adam@23
|
204 if invalid then
|
adam@23
|
205 error <xml>Invalid or expired session</xml>
|
adam@23
|
206 else
|
adam@23
|
207 dml (UPDATE session
|
adam@23
|
208 SET Identifier = {[Some ident]}
|
adam@23
|
209 WHERE Id = {[signup.Session]});
|
adam@23
|
210 signupDetails after
|
adam@23
|
211 | Some (LoggedIn login) =>
|
adam@23
|
212 invalid <- oneRowE1 (SELECT COUNT( * ) = 0
|
adam@23
|
213 FROM session
|
adam@23
|
214 WHERE session.Id = {[login.Session]}
|
adam@23
|
215 AND session.Key = {[login.Key]});
|
adam@23
|
216 if invalid then
|
adam@23
|
217 error <xml>Invalid or expired session</xml>
|
adam@23
|
218 else
|
adam@23
|
219 dml (UPDATE session
|
adam@23
|
220 SET Identifier = {[Some ident]}
|
adam@23
|
221 WHERE Id = {[login.Session]});
|
adam@23
|
222 redirect (bless after)
|
adam@23
|
223 | None => error <xml>Missing session cookie</xml>
|
adam@23
|
224
|
adam@16
|
225 fun newSession () =
|
adam@16
|
226 ses <- nextval sessionIds;
|
adam@16
|
227 now <- now;
|
adam@16
|
228 key <- rand;
|
adam@16
|
229 dml (INSERT INTO session (Id, Key, Identifier, Expires)
|
adam@16
|
230 VALUES ({[ses]}, {[key]}, NULL, {[addSeconds now M.sessionLifetime]}));
|
adam@16
|
231 return {Session = ses, Key = key}
|
adam@16
|
232
|
adam@17
|
233 fun logon after r =
|
adam@16
|
234 ident <- oneOrNoRowsE1 (SELECT (identity.Identifier)
|
adam@16
|
235 FROM identity
|
adam@16
|
236 WHERE identity.User = {[r.User]}
|
adam@16
|
237 LIMIT 1);
|
adam@16
|
238 case ident of
|
adam@16
|
239 None => error <xml>Username not found</xml>
|
adam@16
|
240 | Some ident =>
|
adam@16
|
241 ses <- newSession ();
|
adam@22
|
242 setCookie auth {Value = LoggedIn (r ++ ses),
|
adam@22
|
243 Expires = None,
|
adam@22
|
244 Secure = M.secureCookies};
|
adam@16
|
245 ses <- return ses.Session;
|
adam@23
|
246 if M.fakeId = Some ident then
|
adam@23
|
247 fakeCallback ident after ses
|
adam@23
|
248 else
|
adam@23
|
249 msg <- Openid.authenticate (opCallback after ses)
|
adam@23
|
250 {Association = M.association,
|
adam@23
|
251 Realm = M.realm,
|
adam@23
|
252 Identifier = ident};
|
adam@23
|
253 error <xml>Login with your identity provider failed: {[msg]}</xml>
|
adam@16
|
254
|
adam@16
|
255 fun doSignup after r =
|
adam@16
|
256 ses <- newSession ();
|
adam@22
|
257 setCookie auth {Value = SigningUp ses,
|
adam@22
|
258 Expires = None,
|
adam@22
|
259 Secure = M.secureCookies};
|
adam@16
|
260 ses <- return ses.Session;
|
adam@23
|
261 if M.fakeId = Some r.Identifier then
|
adam@23
|
262 fakeCallback r.Identifier after ses
|
adam@23
|
263 else
|
adam@23
|
264 msg <- Openid.authenticate (opCallback after ses)
|
adam@23
|
265 {Association = M.association,
|
adam@23
|
266 Realm = M.realm,
|
adam@23
|
267 Identifier = r.Identifier};
|
adam@23
|
268 error <xml>Login with your identity provider failed: {[msg]}</xml>
|
adam@16
|
269
|
adam@22
|
270 fun signup after =
|
adam@26
|
271 let
|
adam@27
|
272 fun fixed cls url =
|
adam@26
|
273 let
|
adam@26
|
274 fun doFixedButton () =
|
adam@26
|
275 doSignup after {Identifier = url}
|
adam@26
|
276 in
|
adam@26
|
277 <xml><form class={provider}>
|
adam@27
|
278 <submit class={cls} value="" action={doFixedButton}/>
|
adam@26
|
279 </form></xml>
|
adam@26
|
280 end
|
adam@26
|
281 in
|
adam@26
|
282 wrap "Account Signup" <xml>
|
adam@26
|
283 <p>This web site uses the <b><a href="http://openid.net/">OpenID</a></b> standard, which lets you log in using your account from one of several popular web sites, without revealing your password to us.</p>
|
adam@26
|
284
|
adam@26
|
285 <p>You may click one of these buttons to choose to use your account from that site:</p>
|
adam@27
|
286 {fixed aol "https://openid.aol.com/"}
|
adam@27
|
287 {fixed google "https://www.google.com/accounts/o8/id"}
|
adam@27
|
288 {fixed myspace "https://www.myspace.com/openid"}
|
adam@27
|
289 {fixed yahoo "https://me.yahoo.com/"}
|
adam@26
|
290
|
adam@26
|
291 <p>Visitors familiar with the details of OpenID may also enter their own identifiers:</p>
|
adam@26
|
292 <form>
|
adam@26
|
293 OpenID Identifier: <textbox{#Identifier}/><br/>
|
adam@26
|
294 <submit value="Sign Up" action={doSignup after}/>
|
adam@26
|
295 </form>
|
adam@26
|
296 </xml>
|
adam@26
|
297 end
|
adam@16
|
298 in
|
adam@16
|
299 cur <- current;
|
adam@17
|
300 here <- currentUrl;
|
adam@16
|
301 case cur of
|
kkallio@31
|
302 Some cur => return {Status = <xml>{M.ctlDisplay.User.Status}{[cur]}</xml>,
|
adam@32
|
303 Other = <xml><a link={logout ()}>{M.ctlDisplay.User.Logout}</a></xml>}
|
kkallio@31
|
304 | None => return {Status = <xml>{M.ctlDisplay.Guest.Status}<form><textbox{#User}/> <submit value="Log In" action={logon (show here)}/></form></xml>,
|
adam@32
|
305 Other = <xml><a link={signup (show here)}>{M.ctlDisplay.Guest.Signup}</a></xml>}
|
adam@16
|
306 end
|
adam@16
|
307
|
adam@16
|
308 task periodic 60 = fn () => dml (DELETE FROM session
|
adam@21
|
309 WHERE Expires < CURRENT_TIMESTAMP)
|
adam@16
|
310
|
adam@16
|
311 end
|