=pod =head1 OAuth, RESTy APIs, Microservices, ... =head2 META =head3 Author Thomas Klausner =head3 Email domm AT plix.at =head3 URL http://domm.plix.at =head3 URL_slides http://domm.plix.at/talks/2015_dresden_oauth =head3 Date 2015-05-07 =head3 Location Dresden =head3 Event German Perl Workshop 2015 =head2 /me Thomas Klausner domm http://domm.plix.at @domm_favors_irc =for newslide =for img vienna.pm.org.png =for newslide =for img bicycle.pm.png =for newslide =head2 Microservices Instead of one big monolithic application use several distinct microservices that do one thing and do it well hot new shit =head2 RESTy APIs We're still talking HTTP But instead of producing one big chunk of HTML the backends spit out JSON And the frontend renders the JSON Frontend calls backend APIs using HTTP verbs =for newslide B (I'm very happy about that...) =for newslide People like to argue about what REST is. Content Negotiation, HATEOAS, HTTP Verbs, and 100 more bikesheds I don't care (a lot), I just want to get things done.. RESTB =head2 OAuth2 OAuth2 allows you to give an application some rights to use another application on your behalf. This is very handy for microservices! It can also be used (or abused) as an authentication system ("OpenID Connect") Most people only think of this when we're talking about OAuth2 =head3 Authentication via OAuth2 Log in with Google / Facebook / big-fat-service.com =for newslide UserAgent your-site.com google.com =for newslide UserAgent your-site.com google.com User gets session-id =for newslide UserAgent your-site.com google.com User gets session-id < LOG IN WITH GOOGLE > =for newslide UserAgent your-site.com google.com User gets session-id < LOG IN WITH GOOGLE > Redirect to google.com/o/oauth/$your_client_id =for newslide UserAgent your-site.com google.com User gets session-id < LOG IN WITH GOOGLE > Redirect to google.com/o/oauth/$your_client_id < Enter Password > =for newslide UserAgent your-site.com google.com User gets session-id < LOG IN WITH GOOGLE > Redirect to google.com/o/oauth/$your_client_id < Enter Password > OK! Redirect to your-site.com/postback?code=123xyz =for newslide UserAgent your-site.com google.com User gets session-id < LOG IN WITH GOOGLE > Redirect to google.com/o/oauth/$your_client_id < Enter Password > OK! Redirect to your-site.com/postback?code=123xyz Take the code post it to google with secret =for newslide UserAgent your-site.com google.com User gets session-id < LOG IN WITH GOOGLE > Redirect to google.com/o/oauth/$your_client_id < Enter Password > OK! Redirect to your-site.com/postback?code=123xyz Take the code post it to google with secret Verify secret Return AccessToken =for newslide UserAgent your-site.com google.com User gets session-id < LOG IN WITH GOOGLE > Redirect to google.com/o/oauth/$your_client_id < Enter Password > OK! Redirect to your-site.com/postback?code=123xyz Take the code post it to google with secret Verify secret Return AccessToken Mark user logged in Store AccessToken =for newslide UserAgent your-site.com google.com User gets session-id < LOG IN WITH GOOGLE > Redirect to google.com/o/oauth/$your_client_id < Enter Password > OK! Redirect to your-site.com/postback?code=123xyz Take the code post it to google with secret Verify secret Return AccessToken Mark user logged in Store AccessToken Show some content =for newslide You can watch a free dance performance of this process here: https://www.youtube.com/watch?v=xeGxGnSkSdQ YAPC::Europe 2014 Lightning Talks: OAuth 2.0 Authorization Explained =head4 Results User is logged in at big-fat-service.com (most likely was already logged in) User is logged in at your-site.com without needing a password for your-site.com You have an Access Token for big-fat-service.com =head4 Side note - Flows I showed you the "Web Server Flow". There are a few more flows, the most interesting being the "Popup Flow" You can use this in JavaScript to Login and get an Access Token. You can than store the token in local storage and/or send it to your backend =head3 Another side note - Grants Usually your-site.com asks for a set of permissions to be used on big-fat-service.com Login, Post to Wall, Read List of Friends During the login process, the user is asked if he wants to grant this set of permissions =for newslide =for img grants.png =head3 Using the Access Token B i.e. User does something on your-site.com, and you immediatly post something to facebook B i.e. at midnight, post a summary of users actions to facebook Tweet your Picture of the Day at 10:00 via cron =head4 Examples GET https://www.googleapis.com/plus/v1/people/ me/people/visible?key={YOUR_API_KEY} { "kind": "plus#peopleFeed", "etag": "\"RqKWnRU4WW46-6W3rWhLR9iFZQM/ObDqMibaPxXUs7wb-vH72G2zQbI\"", "title": "Google+ List of Visible People", "totalItems": 0, "items": [ ] } =for newslide Or: GET https://content.googleapis.com/plus/v1/people/me Authorization: Bearer AIzaSyCFj15TpkchL4OUhLD1Q2zgxQnMb7v3fas Bearer Token! We'll take about those in a minute.. =for newslide Only use tokens over SSL A Token is just a fancy password A Token is just a fancy session cookie or is it? =head2 JWT pronounced "jot" - JSON Web Token eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImlzX3Jvb3QiOjF9.ToqKERcPNY1euV-jwpQunfmuN1H3Sj3hRo82MXXF5rw eyJhbGciOiJIUzI1NiJ9. eyJzdWIiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImlzX3Jvb3QiOjF9. ToqKERcPNY1euV-jwpQunfmuN1H3Sj3hRo82MXXF5rw Three parts seperated by dot, each part base64url encoded =for newslide Header, Body ("Claims"), Signature Headers tells us how the Claims are encrypted / signed Body is a hash of so-called claims (e.g. user-id, expiry date, issuer, ..) Signature is a signature =for newslide eyJhbGciOiJIUzI1NiJ9. eyJzdWIiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImlzX3Jvb3QiOjF9. ToqKERcPNY1euV-jwpQunfmuN1H3Sj3hRo82MXXF5rw =for newslide {"alg":"HS256"} eyJzdWIiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImlzX3Jvb3QiOjF9. ToqKERcPNY1euV-jwpQunfmuN1H3Sj3hRo82MXXF5rw =for newslide {"alg":"HS256"} {"exp":1300819380,"is_root":1,"iss":"joe"} ToqKERcPNY1euV-jwpQunfmuN1H3Sj3hRo82MXXF5rw =for newslide {"alg":"HS256"} {"exp":1300819380,"is_root":1,"iss":"joe"} signature based on alg & content =for newslide Or you use a library to verify & unpack it: use Data::Dumper; use JSON::WebToken; my $claims = JSON::WebToken->decode( $token, $secret ); say Dumper $claims; { 'exp' => 1300819380, 'is_root' => 1, 'sub' => 'joe' }; =head3 Claims There are a few official claims, but you can allways add more private claims. =over =item * B = Subject of the token, mostly the user-id / email =item * B = Issuer: who issued the token (eg google.com) =item * B = Audience: whom the token is intended to (eg your service-id) =item * B = Expiration Time =item * and some more timestamps related claims =back =for newslide JWTs are cool Tokens because the can be signed & even encrypted If you are a API and receive a JWT, you can just verify the signature, look at the claims, and do whatever you (or the caller) wants to do. For true stateless backends! =for newslide Of course you can also use a plain random string as a token store that in your storage (DB, session store ..) and do a lookup everytime you get a token But I really like the idea of NOT having to do that =head2 Semi-live Demo =head3 Authorize We wrote our own OAuth2 server which might be a bit of an overkill, but was a lot of fun and a good way to really understand the protocol I'll log in there =for newslide =for img accounts_login.png =for newslide =for img accounts_logged_in.png =head3 Getting a token Now that I'm logged in, I can get a Token =for newslide =for img accounts_issue_token.png =for newslide eyJhbGciOiJIUzI1NiJ9. eyJzdWIiOiJiODA0NGY0MC05ZjQ2LTQxOWUtYWJiYi0zOWY4YjkwY2VhZDMiLCJ leHAiOjE0Mjk3NDkxODIsImlhdCI6MTQyOTcyMDM4MiwiaXNzIjoiYWNjb3VudH MudmFsaWRhZCIsImF1ZCI6IjE0MjBkMjMwLWE0ZTMtNGQyYS1iMGEwLTQ0N2RhZ TUxMTVlZiJ9. ntOgffomGiFC0LlL-5q-TJCCjIlrsMopI2yTxLO1qZk =for newslide perl -Mlocal::lib=local -MJSON::WebToken -MData::Dumper -E ' my $d = JSON::WebToken->decode("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiODA0NGY0MC05ZjQ2LTQxOWUtYWJiYi0zOWY4YjkwY2VhZDMiLCJleHAiOjE0Mjk3NDkxODIsImlhdCI6MTQyOTcyMDM4MiwiaXNzIjoiYWNjb3VudHMudmFsaWRhZCIsImF1ZCI6IjE0MjBkMjMwLWE0ZTMtNGQyYS1iMGEwLTQ0N2RhZTUxMTVlZiJ9.ntOgffomGiFC0LlL-5q-TJCCjIlrsMopI2yTxLO1qZk","*******"); say Dumper $d' =for newslide { 'exp' => 1429749182, 'sub' => 'b8044f40-9f46-419e-abbb-39f8b90cead3', 'aud' => '1420d230-a4e3-4d2a-b0a0-447dae5115ef', 'iat' => 1429720382, 'iss' => 'accounts.validad' } =for newslide Now we can use that token everywhere to use the API I could fax the token to you and you could use it =head3 Using the token I'll use a very simple Comment API we've developed It does comments, but you can also rate arbitrary stuff =for newslide Let's just call the API and GET some public data curl http://localhost:5300/api/604a8c70-4bb4-4658-b2e8-7bd37054fc1b\ /ratings/stars/sector5 =for newslide { "count":"0", "avg":null } =for newslide Now let's PUT to add a rating curl http://localhost:5300/api/604a8c70-4bb4-4658-b2e8-7bd37054fc1b\ /ratings/stars/sector5 -XPUT -d '{ "rating" : 5 }' =for newslide Error 401

Error 401

401 Unauthorized

=for newslide curl http://localhost:5300/api/604a8c70-4bb4-4658-b2e8-7bd37054fc1b\ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiODA0N...\ /ratings/stars/sector5 -XPUT -d '{ "rating" : 5 }' =for newslide { "status": "AdddedRating", "ratings": { "avg": "5.0000000000000000", "count":"1" } } Yay! =head2 Key point Whoever has the token, can act as the user Token can be a JWT, but also just any random string You can pass the token to the JS frontend and directly call any backend that accepts the token (you maybe have to fight with CORS, but that's still easier than fighting against cookies AND CORS) You can store the token in some backend and have it call another backend You can store the token in a commandline script / desktop app I assume you can do stuff with mobile apps, too. =head2 Shut the fuck up and show some code (Die folgenden Slides wollte ich eigentlich heute abend fertig machen, aber es gab eine Schedule-Aenderung...) =for newslide Microservices = lots of small apps which need a lot of shared functionality We put ours into C =head3 OX https://metacpan.org/pod/OX Stevan Little, Jesse Luehrs, infinity interactive "the hardest working two letters in Perl" Bread::Board, Path::Router, and PSGI "The philosophy behind OX is that the building blocks of your web application should just 'click' together, without the overhead of an additional plugin system or 'glue' layer" not monolithic (like other Perl frameworks) Dependency Injection Testable =head3 Plack Middleware The correct place to implement most of the generic part of web apps The Framework is the wrong part! Sorry, Catalyst, Dancer, Mojolicous... (but they are moving a lot of stuff to middlewares, too) =head3 Roles and/or Classes If you already use a class and want to ammend it, put it in a role. If you don't have a class yet, it's better to create a new class than a role. At least that's our current best practice. =head3 What happend to "Shut the fuck up and show some code"?? ok, ok.. =head3 Comments::API package Comments::API; use OX; use Plack::Runner; use OX::RouteBuilder::REST; with 'Validad::Tools::Role::OX::Plack' => { port_offset => 300 }; sub request_class {'Comments::Request'} =for newslide router as { wrap 'Plack::Middleware::ReverseProxy'; wrap 'Plack::Middleware::ReverseProxyPath'; wrap 'Validad::Tools::Plack::Middleware::CORS'; wrap 'Validad::Tools::Plack::Middleware::Error'; wrap 'Singleton', 'Validad::Tools::Plack::Middleware::ExtractBearerToken' => ( secret => 'token_secret', ); wrap 'Singleton', 'Validad::Tools::Plack::Middleware::VerifyApp' => ( schema => 'schema' ); wrap 'Singleton', 'Validad::Tools::Plack::Middleware::RequireApp' => ( allow_without => literal( [ qr{^/api/?$}, qr{^/assets} ] ), ); # not app specific route '/api' => 'root.index'; # needs an app-id route '/comments/:thing_or_uuid' => 'REST.comment_api.collection'; route '/comment/:comment_uuid/edit' => 'REST.comment_api.edit'; route '/ratings/stars/:thing' => 'REST.rating_api.stars'; route '/ratings/thumb/:thing' => 'REST.rating_api.thumb'; }; =for newslide wrap 'Plack::Middleware::ReverseProxy'; wrap 'Plack::Middleware::ReverseProxyPath'; wrap 'Validad::Tools::Plack::Middleware::CORS'; wrap 'Validad::Tools::Plack::Middleware::Error'; =for newslide wrap 'Singleton', 'Validad::Tools::Plack::Middleware::ExtractBearerToken' => ( secret => 'token_secret', ); wrap 'Singleton', 'Validad::Tools::Plack::Middleware::VerifyApp' => ( schema => 'schema' ); wrap 'Singleton', 'Validad::Tools::Plack::Middleware::RequireApp' => ( allow_without => literal( [ qr{^/api/?$}, qr{^/assets} ] ), ); =for newslide # not app specific route '/api' => 'root.index'; # needs an app-id route '/comments/:thing_or_uuid' => 'REST.comment_api.collection'; route '/comment/:comment_uuid/edit' => 'REST.comment_api.edit'; route '/ratings/stars/:thing' => 'REST.rating_api.stars'; route '/ratings/thumb/:thing' => 'REST.rating_api.thumb'; =head3 Validad::Tools::Plack::Middleware::Error sub call { my ( $self, $env ) = @_; my $r; eval { $r = $self->app->($env) }; my $error; my $status = 500; =for newslide if ( my $e = $@ ) { if ( blessed($e) ) { if ( $e->can('message') ) { $error = $e->message; } else { $error = '' . $e; } $status = $e->http_status if $e->can('http_status'); # HTTP::Throwable: $status = $e->status_code if $e->can('status_code'); } else { $error = $e; } } Unexpected Exception =for newslide elsif ( is_error( $r->[0] ) ) { my $raw = $r->[2]; $error = ref($raw) eq 'ARRAY' ? join( '', @$raw ) : $raw; $status = $r->[0]; } It's already a proper Plack error =for newslide else { return $r; } It worked! =for newslide my $req = Plack::Request->new($env); my $orig_res = Plack::Response->new(@$r); if ( $orig_res->content_type =~ m{application/json}i ) { $log->error( $req->uri->as_string . ': Passing on json_error from app' ); return $orig_res->finalize; } =for newslide $log->error( $req->uri->as_string . ': ' . $error ); my $res = Plack::Response->new($status); if ( exists $env->{HTTP_ACCEPT} && $env->{HTTP_ACCEPT} =~ m{application/json}i ) { $res->content_type('application/json'); $res->body( encode_json( { status => 'error', message => "" . $error } ) ); } return JSON if the client wants it =for newslide else { $res->content_type('text/html'); my $content = $self->rendered_error_page( $status, $error ); $res->body($content); } else HTML =head3 Validad::Tools::Plack::Middleware::ExtractBearerToken package Validad::Tools::Plack::Middleware::ExtractBearerToken; use strict; use warnings; use 5.010; use parent 'Plack::Middleware'; use JSON::WebToken; use Plack::Util::Accessor qw(**secret**); use HTTP::Throwable::Factory qw(http_throw); =for newslide sub call { my $self = shift; my $env = shift; my $auth_header = $env->{HTTP_AUTHORIZATION}; if ($auth_header) { =for newslide my ( $type, $token ) = split( /\s+/, $auth_header, 2 ); if ( lc($type) eq 'bearer' && $token ) { my $claims = eval { return JSON::WebToken->decode( $token, **$self->secret** ); }; =for newslide # Comments::API wrap 'Singleton', 'Validad::Tools::Plack::Middleware::ExtractBearerToken' => ( **secret** => 'token_secret', ); =for newslide my $claims = eval { return JSON::WebToken->decode( $token, $self->secret ); }; if ( !$claims || $@ ) { my $err = ref($@) ? $@->code : $@; $err ||= 'unknown error'; http_throw( BadRequest => { message => "Cannot decode bearer token: $err" } ); } =for newslide if ( $claims->{exp} + 30 < time() ) { # allow for 30 secs clock skew http_throw( { status_code => 419, reason => 'Authentication Timeout', message => 'Bearer token is expired', } ); } =for newslide $env->{'psgix.oauth.token'} = $token; $env->{'psgix.oauth.claims'} = $claims; } } return $self->app->($env); } =for newslide $env->{'psgix.oauth.token'} = $token; $env->{'psgix.oauth.claims'} = $claims; I can now use that in my actuall app code $req->env->{'psgix.oauth.token'} But I cannot be bothered to rember this exact string.. =head3 Validad::Tools::Role::OX::Request::BearerToken package Validad::Tools::Role::OX::Request::BearerToken; use 5.014; use strict; use warnings; use Moose::Role; use HTTP::Throwable::Factory qw(http_throw); =for newslide sub oauth_token { my $self = shift; return $self->env->{'psgix.oauth.token'}; } =for newslide package Comments::API; ... sub request_class {'Comments::Request'} =for newslide package Comments::Request; use 5.014; use Moose; extends 'Validad::Tools::OX::Request'; with qw( Validad::Tools::Role::OX::Request::AppHal Validad::Tools::Role::OX::Request::BearerToken ); __PACKAGE__->meta->make_immutable; =for newslide # in controller action $req->oauth_token; # better than $req->env->{'psgix.oauth.token'} But C provides a few more methods eg C =for newslide this is the controller actions that handles this PUT curl http://localhost:5300/api/604a8c70-4bb4-4658-b2e8-7bd37054fc1b\ /ratings/stars/sector5 -XPUT -d '{ "rating" : 5 }' =for newslide sub stars_PUT { my ( $self, $req, $thing ) = @_; my $usr = $req->**requires_oauth_claim_sub**; =for newslide sub requires_oauth_claim_sub { my $self = shift; my $sub = $self->oauth_claim_sub; return $sub if $sub; http_throw( 'Unauthorized' => { www_authenticate => 'bearer' } ); } =for newslide sub oauth_claim_sub { my $self = shift; my $claims = $self->oauth_claims; return unless $claims && ref($claims) eq 'HASH'; return $claims->{sub}; } =for newslide sub oauth_claims { my $self = shift; return $self->env->{'psgix.oauth.claims'}; } =for newslide sub stars_PUT { my ( $self, $req, $thing ) = @_; my $usr = $req->requires_oauth_claim_sub; my $data = $req->decoded_json_content_cloneable; if ($self->rating_stars_model->rate_thing( $req->app_id, $thing, $usr, $data->{rating} ) ) { my $ratings = $self->rating_stars_model->get_ratings_of_thing( $req->app_id, $thing ); return $req->new_ng_response( { status => 'AdddedRating', ratings => $ratings } ); } else { return $req->new_ng_response( { status => 'Error', message => 'unknown error while rating' } ); } } =head3 Key point Plack Middlewares rule! =head2 Questions / Discussion