├── 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/ 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 | --------------------------------------------------------------------------------