├── README
├── hmac-sha1
├── oauth.xqy
└── schema
├── oauth.rnc
└── oauth.xsd
/README:
--------------------------------------------------------------------------------
1 | Hello,
2 |
3 | This repository contains an OAuth implementation written in XQuery. It
4 | relies on several MarkLogic extensions at the moment, so if you're not
5 | running on MarkLogic server, you'll have to write a few bits.
6 |
7 | Also, there's no native implementation of the HMAC-SHA1 signing algorithm
8 | at the moment, so this script relies on a web service to compute that.
9 | If you want to setup the web service yourself, my current implementation
10 | is in perl, hmac-sha1.
11 |
12 | Docs, etc. to follow. (In the fullness of time, like the next ice age,
13 | probably. If you have questions, feel free to ask.)
14 |
15 | P.S. There's definitely a bug or two at the moment, some requests
16 | succeed others report invalid signature. I'll fix that as soon as I
17 | can figure it out.
18 |
19 | --norm
20 | ndw@nwalsh.com
21 |
--------------------------------------------------------------------------------
/hmac-sha1:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl
2 |
3 | # This perl script computes the HMAC-SHA1 signature of the provided 'data'
4 | # with the provided 'key'.
5 |
6 | use strict;
7 | use English;
8 | use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
9 | use MIME::Base64;
10 | use CGI;
11 |
12 | print "Content-type: application/xml\n\n";
13 |
14 | my $cgi = new CGI;
15 | my $key = $cgi->param('key');
16 | my $data = $cgi->param('data');
17 |
18 | my $digest = hmac_sha1($data, $key);
19 | my $encoded = encode_base64($digest);
20 | chop $encoded; # don't want the newline
21 | my $hex = hmac_sha1_hex($data, $key);
22 |
23 | print "\n";
24 | print "", escape($key), "\n";
25 | print "", escape($data), "\n";
26 | print "$encoded\n";
27 | print "$hex\n";
28 | print "\n";
29 |
30 | # ----------------------------------------------------------------------
31 |
32 | sub escape {
33 | local $_ = shift;
34 | s/&/&/sg;
35 | s/</sg;
36 | return $_;
37 | }
38 |
--------------------------------------------------------------------------------
/oauth.xqy:
--------------------------------------------------------------------------------
1 | xquery version "1.0-ml";
2 |
3 | module namespace oa="http://marklogic.com/ns/oauth";
4 |
5 | declare namespace xh="xdmp:http";
6 |
7 | declare default function namespace "http://www.w3.org/2005/xpath-functions";
8 |
9 | declare option xdmp:mapping "false";
10 |
11 | (:
12 | let $service :=
13 |
14 |
15 | http://twitter.com/oauth/request_token
16 | GET
17 |
18 |
19 | http://twitter.com/oauth/authorize
20 |
21 |
22 | http://twitter.com/oauth/authenticate
23 | force_login=true
24 |
25 |
26 | http://twitter.com/oauth/access_token
27 | POST
28 |
29 |
30 | HMAC-SHA1
31 |
32 | 1.0
33 |
34 | YOUR-CONSUMER-KEY
35 | YOUR-CONSUMER-SECRET
36 |
37 |
38 | :)
39 |
40 | declare function oa:timestamp() as xs:unsignedLong {
41 | let $epoch := xs:dateTime('1970-01-01T00:00:00Z')
42 | let $now := current-dateTime()
43 | let $d := $now - $epoch
44 | let $seconds
45 | := 86400 * days-from-duration($d)
46 | + 3600 * hours-from-duration($d)
47 | + 60 * minutes-from-duration($d)
48 | + seconds-from-duration($d)
49 | return
50 | xs:unsignedLong($seconds)
51 | };
52 |
53 | declare function oa:sign($key as xs:string, $data as xs:string) as xs:string {
54 | let $uri := concat("http://localhost:8190/cgi-bin/hmac-sha1?",
55 | "key=", encode-for-uri($key),
56 | "&data=",encode-for-uri($data))
57 | let $resp := xdmp:http-get($uri)
58 | return
59 | string($resp/digest/hashb64)
60 | };
61 |
62 | declare function oa:signature-method(
63 | $service as element(oa:service-provider)
64 | ) as xs:string
65 | {
66 | if ($service/oa:signature-methods/oa:method = "HMAC-SHA1")
67 | then "HMAC-SHA1"
68 | else error(xs:QName("oa:BADSIGMETHOD"),
69 | "Service must support 'HMAC-SHA1' signatures.")
70 | };
71 |
72 | declare function oa:http-method(
73 | $proposed-method as xs:string
74 | ) as xs:string
75 | {
76 | if (upper-case($proposed-method) = "GET")
77 | then "GET"
78 | else if (upper-case($proposed-method) = "POST")
79 | then "POST"
80 | else error(xs:QName("oa:BADHTTPMETHOD"),
81 | "Service must use HTTP GET or POST.")
82 | };
83 |
84 | declare function oa:request-token(
85 | $service as element(oa:service-provider),
86 | $callback as xs:string?)
87 | as element(oa:request-token)
88 | {
89 | let $options := if (empty($callback))
90 | then ()
91 | else
92 |
93 | {$callback}
94 |
95 | let $data
96 | := oa:signed-request($service,
97 | $service/oa:request-token/oa:method,
98 | $service/oa:request-token/oa:uri,
99 | $options, (), ())
100 | return
101 |
102 | { if ($data/oa:error)
103 | then
104 | $data/*
105 | else
106 | for $pair in tokenize($data, "&")
107 | return
108 | element { concat("oa:", substring-before($pair, '=')) }
109 | { substring-after($pair, '=') }
110 | }
111 |
112 | };
113 |
114 | declare function oa:access-token(
115 | $service as element(oa:service-provider),
116 | $request as element(oa:request-token),
117 | $verifier as xs:string)
118 | as element(oa:access-token)
119 | {
120 | let $options := {$verifier}
121 | let $data
122 | := oa:signed-request($service,
123 | $service/oa:access-token/oa:method,
124 | $service/oa:access-token/oa:uri,
125 | $options,
126 | $request/oa:oauth_token,
127 | $request/oa:oaauth_token_secret)
128 | return
129 |
130 | { if ($data/oa:error)
131 | then
132 | $data/*
133 | else
134 | for $pair in tokenize($data, "&")
135 | return
136 | element { concat("oa:", substring-before($pair, '=')) }
137 | { substring-after($pair, '=') }
138 | }
139 |
140 | };
141 |
142 | declare function oa:signed-request(
143 | $service as element(oa:service-provider),
144 | $method as xs:string,
145 | $serviceuri as xs:string,
146 | $options as element(oa:options)?,
147 | $token as xs:string?,
148 | $secret as xs:string?)
149 | as element(oa:response)
150 | {
151 | let $realm := string($service/@realm)
152 | let $noncei := xdmp:hash64(concat(current-dateTime(),string(xdmp:random())))
153 | let $nonce := xdmp:integer-to-hex($noncei)
154 | let $stamp := oa:timestamp()
155 | let $key := string($service/oa:authentication/oa:consumer-key)
156 | let $sigkey := concat($service/oa:authentication/oa:consumer-key-secret,
157 | "&", if (empty($secret)) then "" else $secret)
158 | let $version := string($service/oa:oauth-version)
159 | let $sigmethod := oa:signature-method($service)
160 | let $httpmethod := oa:http-method($method)
161 |
162 | let $sigstruct
163 | :=
164 | {$key}
165 | {$nonce}
166 | {$sigmethod}
167 | {$stamp}
168 | {$version}
169 | { if (not(empty($token)))
170 | then {$token}
171 | else ()
172 | }
173 | { if (not(empty($options)))
174 | then $options/*
175 | else ()
176 | }
177 |
178 |
179 | let $encparams
180 | := for $field in $sigstruct/*
181 | order by local-name($field)
182 | return
183 | concat(local-name($field), "=", encode-for-uri(string($field)))
184 |
185 | let $sigbase := string-join(($httpmethod, encode-for-uri($serviceuri),
186 | encode-for-uri(string-join($encparams,"&"))), "&")
187 |
188 | let $signature := encode-for-uri(oa:sign($sigkey, $sigbase))
189 |
190 | (: This is a bit of a pragmatic hack, what's the real answer? :)
191 | let $authfields := $sigstruct/*[starts-with(local-name(.), "oauth_")
192 | and not(self::oauth_callback)]
193 |
194 | let $authheader := concat("OAuth realm="", $service/@realm, "", ",
195 | "oauth_signature="", $signature, "", ",
196 | string-join(
197 | for $field in $authfields
198 | return
199 | concat(local-name($field),"="", encode-for-uri($field), """),
200 | ", "))
201 |
202 | let $uriparam := for $field in $options/*
203 | return
204 | concat(local-name($field),"=",encode-for-uri($field))
205 |
206 | (: This strikes me as slightly weird. Twitter wants the parameters passed
207 | encoded in the URI even for a POST. I don't know if that's a Twitter
208 | quirk or the natural way that OAuth apps work. Anyway, if you find
209 | this library isn't working for some other OAuth'd API, you might want
210 | to play with this bit.
211 |
212 | let $requri := if ($httpmethod = "GET")
213 | then concat($serviceuri,
214 | if (empty($uriparam)) then ''
215 | else concat("?",string-join($uriparam,"&")))
216 | else $serviceuri
217 |
218 | let $data := if ($httpmethod = "POST" and not(empty($uriparam)))
219 | then {string-join($uriparam,"&")}
220 | else ()
221 | :)
222 |
223 | let $requri := concat($serviceuri,
224 | if (empty($uriparam)) then ''
225 | else concat("?",string-join($uriparam,"&")))
226 |
227 | let $data := ()
228 |
229 | let $options :=
230 |
231 | {$authheader}
232 |
233 | { $data }
234 |
235 |
236 | let $tokenreq := if ($httpmethod = "GET")
237 | then xdmp:http-get($requri, $options)
238 | else xdmp:http-post($requri, $options)
239 |
240 | (:
241 | let $trace := xdmp:log(concat("requri: ", $requri))
242 | let $trace := xdmp:log(concat("sigbse: ", $sigbase))
243 | let $trace := xdmp:log($options)
244 | let $trace := xdmp:log($tokenreq[2])
245 | :)
246 |
247 | return
248 |
249 | { if (string($tokenreq[1]/xh:code) != "200")
250 | then
251 | ({$tokenreq[1]},
252 | {$tokenreq[2]})
253 | else
254 | $tokenreq[2]
255 | }
256 |
257 | };
258 |
--------------------------------------------------------------------------------
/schema/oauth.rnc:
--------------------------------------------------------------------------------
1 | default namespace = "http://marklogic.com/ns/oauth"
2 |
3 | start = ServiceProvider | RequestToken | Options | Response
4 |
5 | ServiceProvider =
6 | element service-provider {
7 | attribute realm { text },
8 | (RequestToken
9 | & UserAuthorization
10 | & UserAuthentication
11 | & AccessToken
12 | & SignatureMethods
13 | & OAuthVersion
14 | & Authentication)
15 | }
16 |
17 | RequestToken =
18 | element request-token {
19 | URI & Method
20 | }
21 |
22 | URI = element uri { text }
23 |
24 | Method = element method { text }
25 |
26 | UserAuthorization =
27 | element user-authorization {
28 | (URI)
29 | }
30 |
31 | UserAuthentication =
32 | element user-authentication {
33 | (URI, AdditionalParams)
34 | }
35 |
36 | AdditionalParams = element additional-params { text }
37 |
38 | AccessToken =
39 | element access-token {
40 | (URI & Method)
41 | }
42 |
43 | SignatureMethods =
44 | element signature-methods {
45 | Method+
46 | }
47 |
48 | OAuthVersion = element oauth-version { text }
49 |
50 | Authentication =
51 | element authentication {
52 | (ConsumerKey & ConsumerKeySecret)
53 | }
54 |
55 | ConsumerKey = element consumer-key { text }
56 | ConsumerKeySecret = element consumer-key-secret { text }
57 |
58 | AnyElement = element * { AnyElement* | text }
59 |
60 | Options =
61 | element options {
62 | element * { text }+
63 | }
64 |
65 | Response =
66 | element response {
67 | AnyElement?
68 | }
69 |
--------------------------------------------------------------------------------
/schema/oauth.xsd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------