|
|
|
|
|
#ifdef SOCIAL_REQUEST_DEBUG |
# define TRACE(X...) werror("%s:%d: %s", basename(__FILE__),__LINE__,sprintf(X)) |
#else |
# define TRACE(X...) 0 |
#endif |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Base `()(string client_id, string client_secret, void|string redirect_uri, |
void|string|array(string)|multiset(string) scope) |
{ |
return Base(client_id, client_secret, redirect_uri, scope); |
} |
|
|
class Base |
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected void create(string client_id, |
string client_secret, |
void|string redirect_uri, |
void|string|array(string)|multiset(string) scope) |
{ |
_client_id = client_id; |
_client_secret = client_secret; |
_redirect_uri = redirect_uri || _redirect_uri; |
_scope = scope || _scope; |
} |
|
|
enum GrantType { |
|
GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code", |
|
|
GRANT_TYPE_IMPLICIT = "implicit", |
|
|
GRANT_TYPE_PASSWORD = "password", |
|
|
GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials", |
|
|
GRANT_TYPE_JWT = "urn:ietf:params:oauth:grant-type:jwt-bearer", |
|
|
GRANT_TYPE_REFRESH_TOKEN = "refresh_token" |
} |
|
|
enum ResponseType { |
|
RESPONSE_TYPE_CODE = "code", |
|
|
RESPONSE_TYPE_TOKEN = "token" |
} |
|
|
string `access_token() |
{ |
return gettable->access_token; |
} |
|
|
|
|
void `access_token=(string value) |
{ |
gettable->access_token = value; |
gettable->created = time(); |
gettable->expires = time() + (3600); |
} |
|
|
string `refresh_token() |
{ |
return gettable->refresh_token; |
} |
|
|
string `token_type() |
{ |
return gettable->token_type; |
} |
|
|
Calendar.Second `expires() |
{ |
return gettable->expires && Calendar.Second("unix", gettable->expires); |
} |
|
|
Calendar.Second `created() |
{ |
return Calendar.Second("unix", gettable->created); |
} |
|
|
mapping `user() |
{ |
return gettable->user; |
} |
|
|
string get_client_id() |
{ |
return _client_id; |
} |
|
|
string get_client_secret() |
{ |
return _client_secret; |
} |
|
|
string get_redirect_uri() |
{ |
return _redirect_uri; |
} |
|
|
string get_grant_type() |
{ |
return _grant_type; |
} |
|
|
|
|
void set_grant_type(GrantType type) |
{ |
_grant_type = type; |
} |
|
#if constant(Crypto.RSA.State) // Pike 8.0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
string get_token_from_jwt(string jwt, string token_endpoint, string|void sub, |
void|function(bool,string:void) async_cb) |
{ |
mapping j = Standards.JSON.decode(jwt); |
mapping header = ([ "alg" : "RS256", "typ" : "JWT" ]); |
|
int now = time(); |
int exp = now + 3600; |
|
mapping claim = ([ |
"iss" : j->client_email, |
"scope" : get_valid_scopes(_scope), |
"aud" : token_endpoint, |
"exp" : exp, |
"iat" : now |
]); |
|
if (sub) { |
claim->sub = sub; |
} |
|
string s = base64url_encode(Standards.JSON.encode(header)); |
s += "." + base64url_encode(Standards.JSON.encode(claim)); |
|
string key = |
|
Standards.PEM.simple_decode(j->private_key); |
|
|
|
|
object x = [object(Standards.ASN1.Types.Sequence)] |
Standards.ASN1.Decode.simple_der_decode(key); |
|
string ss; |
|
|
Crypto.RSA.State state; |
state = Standards.PKCS.RSA.parse_private_key(x->elements[-1]->value); |
ss = state->pkcs_sign(s, Crypto.SHA256); |
|
|
|
|
|
|
s += "." + base64url_encode(ss); |
|
string body = "grant_type="+Protocols.HTTP.uri_encode(GRANT_TYPE_JWT)+"&"+ |
"assertion="+Protocols.HTTP.uri_encode(s); |
|
if (async_cb) { |
Protocols.HTTP.Query q = Protocols.HTTP.Query(); |
q->set_callbacks( |
lambda (Protocols.HTTP.Query qq, mixed ... args) { |
if (qq->status == 200) { |
mapping res = Standards.JSON.decode(q->data()); |
if (!decode_access_token_response(q->data())) { |
async_cb(false, sprintf("Bad result! Expected an access_token " |
"but none were found!\nData: %s.\n", |
q->data())); |
return; |
} |
|
async_cb(true, encode_value(gettable)); |
} |
}, |
lambda (Protocols.HTTP.Query qq, mixed ... args) { |
async_cb(false, "Connection failed!"); |
} |
); |
|
Protocols.HTTP.do_async_method("POST", token_endpoint, 0, request_headers, |
q, body); |
return 0; |
} |
else { |
Protocols.HTTP.Query q; |
q = Protocols.HTTP.do_method("POST", token_endpoint, 0, request_headers, |
0, body); |
|
if (q->status == 200) { |
mapping res = Standards.JSON.decode(q->data()); |
if (!decode_access_token_response(q->data())) { |
error("Bad result! Expected an access_token but none were found!" |
"\nData: %s.\n", q->data()); |
} |
|
return encode_value(gettable); |
} |
|
string ee = try_get_error(q->data()); |
error("Bad status (%d) in response: %s! ", |
q->status, ee||"Unknown error"); |
} |
} |
#endif /* Crypto.RSA.State */ |
|
protected string base64url_encode(string s) |
{ |
s = MIME.encode_base64(s, 1); |
s = replace(s, ([ "==" : "", "+" : "-", "-" : "_" ])); |
return s; |
} |
|
|
|
|
void set_redirect_uri(string uri) |
{ |
_redirect_uri = uri; |
} |
|
|
multiset list_valid_scopes() |
{ |
return valid_scopes; |
} |
|
|
|
|
|
void set_access_type(string access_type) |
{ |
_access_type = access_type; |
} |
|
|
string get_access_type() |
{ |
return _access_type; |
} |
|
|
void set_scope(string scope) |
{ |
_scope = scope; |
} |
|
|
mixed get_scope() |
{ |
return _scope; |
} |
|
|
|
|
int(0..1) has_scope(string scope) |
{ |
if (!_scope || !sizeof(_scope)) |
return 0; |
|
string sp = search(_scope, ",") > -1 ? "," : |
search(_scope, " ") > -1 ? " " : ""; |
array p = map(_scope/sp, String.trim_all_whites); |
|
return has_value(p, scope); |
} |
|
|
|
|
|
|
|
|
|
|
|
|
this_program set_from_cookie(string encoded_value) |
{ |
mixed e = catch { |
gettable = decode_value(encoded_value); |
|
if (gettable->scope) |
_scope = gettable->scope; |
|
if (gettable->access_type) |
_access_type = gettable->access_type; |
|
return this; |
}; |
|
error("Unable to decode cookie! %s. ", describe_error(e)); |
} |
|
|
|
|
|
|
|
string get_auth_uri(string auth_uri, void|mapping args) |
{ |
Auth.Params p = Auth.Params(Auth.Param("client_id", _client_id), |
Auth.Param("response_type", _response_type)); |
|
if (args && args->redirect_uri || _redirect_uri) |
p += Auth.Param("redirect_uri", |
args && args->redirect_uri || _redirect_uri); |
|
if (STATE) |
p += Auth.Param("state", (string) Standards.UUID.make_version4()); |
|
if (args && args->scope || _scope) { |
string sc = get_valid_scopes(args && args->scope || _scope); |
|
if (sc && sizeof(sc)) { |
_scope = sc; |
p += Auth.Param("scope", sc); |
} |
} |
|
if (args && args->access_type || _access_type) { |
p += Auth.Param("access_type", args && args->access_type || _access_type); |
|
if (!_access_type && args && args->access_type) |
_access_type = args->access_type; |
} |
|
if (args) { |
m_delete(args, "scope"); |
m_delete(args, "redirect_uri"); |
p += args; |
} |
|
TRACE("auth_uri(%s)\n", (string) p["redirect_uri"]); |
|
return auth_uri + "?" + p->to_query(); |
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
string request_access_token(string oauth_token_uri, string code, |
void|function(bool,string:void) async_cb) |
{ |
TRACE("request_access_token: %O, %O\n", oauth_token_uri, code); |
|
Auth.Params p = get_default_params(); |
p += Auth.Param("code", code); |
|
if (async_cb) { |
do_query(oauth_token_uri, p, async_cb); |
return 0; |
} |
else { |
if (string s = do_query(oauth_token_uri, p)) |
return s; |
|
error("Failed getting access token! "); |
} |
} |
|
|
|
|
|
|
|
|
|
|
|
|
string refresh_access_token(string oauth_token_uri, |
void|function(bool,string:void) async_cb) |
{ |
TRACE("Refresh: %s @ %s\n", gettable->refresh_token, oauth_token_uri); |
|
if (!gettable->refresh_token) |
error("No refresh_token in object! "); |
|
Auth.Params p = get_default_params(GRANT_TYPE_REFRESH_TOKEN); |
p += Auth.Param("refresh_token", gettable->refresh_token); |
|
if (async_cb) { |
do_query(oauth_token_uri, p, async_cb); |
return 0; |
} |
else { |
if (string s = do_query(oauth_token_uri, p, async_cb)) { |
TRACE("Got result: %O\n", s); |
return s; |
} |
|
error("Failed refreshing access token! "); |
} |
} |
|
|
|
|
|
|
|
|
|
|
|
|
protected string do_query(string oauth_token_uri, Auth.Params p, |
void|function(bool,string:void) async_cb) |
{ |
int qpos = 0; |
|
if ((qpos = search(oauth_token_uri, "?")) > -1) { |
|
oauth_token_uri = oauth_token_uri[..qpos]; |
} |
|
TRACE("params: %O\n", p); |
TRACE("request_access_token(%s?%s)\n", oauth_token_uri, (string) p); |
|
if (async_cb) { |
Protocols.HTTP.Query q = Protocols.HTTP.Query(); |
q->set_callbacks( |
lambda (Protocols.HTTP.Query qq, mixed ... args) { |
if (q->status != 200) { |
string emsg = sprintf("Bad status (%d) in HTTP response! ", |
q->status); |
if (mapping reason = try_get_error(q->data())) |
emsg += sprintf("Reason: %O!\n", reason); |
|
async_cb(false, emsg); |
} |
else { |
if (decode_access_token_response(q->data())) |
async_cb(true, encode_value(gettable)); |
else |
async_cb(false, "Failed decoding response!"); |
} |
}, |
|
lambda (Protocols.HTTP.Query qq, mixed ... args) { |
TRACE("Failed callback\n"); |
async_cb(false, "Connection failed!"); |
} |
); |
|
Protocols.HTTP.do_async_method("POST", oauth_token_uri, 0, |
request_headers, q, p->to_query()); |
|
return 0; |
} |
else { |
Protocols.HTTP.Session sess = Protocols.HTTP.Session(); |
Protocols.HTTP.Session.Request q; |
q = sess->post_url(oauth_token_uri, p->to_mapping()); |
|
TRACE("Query OK: %O : %O : %s\n", q, q->status(), q->data()); |
|
string c = q && q->data(); |
|
if (q->status() != 200) { |
string emsg = sprintf("Bad status (%d) in HTTP response! ", |
q->status()); |
|
if (mapping reason = try_get_error(c)) |
emsg += sprintf("Reason: %O!\n", reason); |
|
error(emsg); |
} |
|
TRACE("Got data: %O\n", c); |
|
if (decode_access_token_response(c)) |
return encode_value(gettable); |
} |
} |
|
|
|
|
protected Auth.Params get_default_params(void|string grant_type) |
{ |
Auth.Params p; |
p = Auth.Params(Auth.Param("client_id", _client_id), |
Auth.Param("redirect_uri", _redirect_uri), |
Auth.Param("client_secret", _client_secret), |
Auth.Param("grant_type", grant_type || _grant_type)); |
if (STATE) { |
p += Auth.Param("state", (string)Standards.UUID.make_version4()); |
} |
|
return p; |
} |
|
|
|
|
|
int(0..1) is_renewable() |
{ |
return !!gettable->refresh_token; |
} |
|
|
int(0..1) is_expired() |
{ |
|
if (gettable->created && !gettable->expires) |
return 0; |
|
return gettable->expires ? time() > gettable->expires : 1; |
} |
|
|
int(0..1) is_authenticated() |
{ |
return !!gettable->access_token && !is_expired(); |
} |
|
|
|
|
|
|
mixed cast(string how) |
{ |
switch (how) { |
case "string": return gettable->access_token; |
case "int": return gettable->expires; |
case "mapping": return gettable; |
} |
|
error("Can't cast %O to %s! ", object_program(this), how); |
} |
|
|
string _sprintf(int t) |
{ |
switch (t) { |
case 's': return gettable->access_token; |
} |
|
return sprintf("%O(%O, %O, %O, %O)", |
object_program(this), gettable->access_token, |
_redirect_uri, |
gettable->created && |
Calendar.Second("unix", gettable->created), |
gettable->expires && |
Calendar.Second("unix", gettable->expires)); |
} |
|
|
|
|
|
|
|
protected multiset(string) valid_scopes = (<>); |
|
|
protected constant VERSION = "1.0"; |
|
|
protected constant USER_AGENT = "Mozilla 4.0 (Pike OAuth2 Client " + |
VERSION + ")"; |
|
|
|
|
protected constant STATE = 0; |
|
|
protected string _client_id; |
|
|
protected string _client_secret; |
|
|
protected string _redirect_uri; |
|
|
protected string|array(string)|multiset(string) _scope; |
|
|
protected string _access_type; |
|
|
|
|
|
|
|
|
|
|
|
protected string _grant_type = GRANT_TYPE_AUTHORIZATION_CODE; |
|
|
|
|
|
|
|
protected string _response_type = RESPONSE_TYPE_CODE; |
|
|
protected mapping request_headers = ([ |
"User-Agent" : USER_AGENT, |
"Content-Type" : "application/x-www-form-urlencoded" |
]); |
|
protected constant json_decode = Standards.JSON.decode; |
protected constant Params = Auth.Params; |
protected constant Param = Auth.Param; |
|
protected mapping gettable = ([ "access_token" : 0, |
"refresh_token" : 0, |
"expires" : 0, |
"created" : 0, |
"token_type" : 0 ]); |
|
|
|
|
|
|
|
protected string get_valid_scopes(string|array(string)|multiset(string) s) |
{ |
if (!s) return ""; |
|
array r = ({}); |
|
if (stringp(s)) |
s = map(s/",", String.trim_all_whites); |
|
if (multisetp(s)) |
s = (array) s; |
|
if (!sizeof(valid_scopes)) |
r = s; |
|
foreach (s, string x) { |
if (valid_scopes[x]) |
r += ({ x }); |
} |
|
return r*" "; |
} |
|
|
|
|
|
|
|
protected int(0..1) decode_access_token_response(string r) |
{ |
if (!r) return 0; |
|
TRACE("Decode response: %s\n", r); |
|
mapping v = ([]); |
|
if (has_prefix(r, "access_token")) { |
foreach (r/"&", string s) { |
sscanf(s, "%s=%s", string key, string val); |
v[key] = val; |
} |
} |
else { |
if (catch(v = json_decode(r))) |
return 0; |
} |
|
if (!v->access_token) |
return 0; |
|
gettable->scope = _scope; |
gettable->created = time(); |
|
foreach (v; string key; string val) { |
if (search(key, "expires") > -1) |
gettable->expires = gettable->created + (int)val; |
else |
gettable[key] = val; |
} |
|
if (_access_type) { |
gettable->access_type = _access_type; |
} |
|
return 1; |
} |
|
|
|
private mixed try_get_error(string data) |
{ |
catch { |
mixed x = json_decode(data); |
return x->error; |
}; |
} |
|
#if 0 |
|
|
|
|
|
|
protected mapping parse_signed_request(string sign) |
{ |
sscanf(sign, "%s.%s", string sig, string payload); |
|
function url_decode = lambda (string s) { |
return MIME.decode_base64(replace(s, ({ "-", "_" }), ({ "+", "/" }))); |
}; |
|
sig = url_decode(sig); |
mapping data = json_decode(url_decode(payload)); |
|
if (upper_case(data->algorithm) != "HMAC-SHA256") |
error("Unknown algorithm. Expected HMAC-SHA256"); |
|
string expected_sig; |
|
#if constant(Crypto.HMAC) |
# if constant(Crypto.SHA256) |
expected_sig = Crypto.HMAC(Crypto.SHA256)(payload)(_client_secret); |
# else |
error("No Crypto.SHA256 available in this Pike build! "); |
# endif |
#else |
error("Not implemented in this Pike version! "); |
#endif |
|
if (sig != expected_sig) |
error("Badly signed signature. "); |
|
return data; |
} |
#endif |
} |
|
|
|
class Client |
{ |
inherit Base; |
|
|
constant OAUTH_AUTH_URI = 0; |
|
|
constant OAUTH_TOKEN_URI = 0; |
|
|
protected constant DEFAULT_SCOPE = 0; |
|
protected void create(string client_id, |
string client_secret, |
void|string redirect_uri, |
void|string|array(string)|multiset(string) scope) |
{ |
::create(client_id, client_secret, redirect_uri, scope); |
} |
|
#if constant(Crypto.RSA.State) |
|
string get_token_from_jwt(string jwt, void|string sub, |
void|function(bool,string:void) async_cb) |
{ |
return ::get_token_from_jwt(jwt, OAUTH_TOKEN_URI, sub, async_cb); |
} |
#endif |
|
|
|
|
|
string get_auth_uri(void|mapping args) |
{ |
if ((args && !args->scope || !args) && DEFAULT_SCOPE) { |
if (!args) args = ([]); |
args->scope = DEFAULT_SCOPE; |
} |
|
return ::get_auth_uri(OAUTH_AUTH_URI, args); |
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
string request_access_token(string code, |
void|function(bool,string:void) async_cb) |
{ |
return ::request_access_token(OAUTH_TOKEN_URI, code, async_cb); |
} |
|
|
|
|
|
|
|
|
|
|
string refresh_access_token(void|function(bool,string:void) async_cb) |
{ |
return ::refresh_access_token(OAUTH_TOKEN_URI, async_cb); |
} |
} |
|
|