Search code examples
perloauth-2.0mojolicious

How do I get refresh tokens with Mojolicious::Plugin::OAuth2


I am a happy user of Mojolicious::Plugin::OAuth2, but there is a but: I can get an access token without a problem, but I have no idea on how to get a refresh one. The documentation is a bit terse and I could not find examples in the wild.

Currently I do this:

plugin OAuth2 => {
          providers => {
                google => {
                       key    => 'somekey',
                       secret => 'somesecret',
                       redirect => 'http://localhost:3000/login/google',
                       access_type => 'offline',
                       scope => join ' ', qw|some scopes|,
                      }
                   }
         };

get '/' => sub {
    my $c = shift;
    $c->render(template => 'login');
};

get '/done' => sub {
    my $c = shift;
    $c->render(text => 'done: ' . $c->session('token'));
};


get '/login/google' => sub {
    my $c = shift;
    my $otx = $c->render_later->tx;

    my $args = { redirect_uri => 'http://localhost:3000/login/google' };

    $c->oauth2->get_token_p(google => $args)
    ->then(sub {
           my $otx = $otx;
           return unless my $res = shift;
           $c->session(token => $res->{access_token});
           1;
           })
    ->then(sub {
           my $tx = shift;
           my $ua = $c->app->ua;
           my $url = 'https://www.googleapis.com/userinfo/v2/me';
           my $tx = $ua->build_tx(GET => $url);
           $tx->req->headers->authorization('Bearer ' . $c->session('token'));
           return $ua->start_p($tx);
           })
    ->then(sub {
           my $tx = shift;
           my $otx = $otx;

           my $data = $tx->res->json;
           $c->app->log->info($tx->res->body);
           $c->app->log->info(dumper $tx->res->json);
           $c->redirect_to('/done');
           })
    ->catch(sub {
            my $err = shift;
            $c->log->info($err);
            $c->render(text => $err);
        });
};

(sorry for the dump) which is pretty much the standard flow for Mojolicious::Plugin::OAuth2.

The response from Google however does not contain any refresh token as far as I can see, nor can I figure out how to ask for one - inserting $c->oauth2->get_refresh_token_p($provider_name => \%args); somewhere in the middle gives me a bad request response.

So, how should I do this so it works ok?


Solution

  • If you set access_type => 'offline' when creating the OAuth2 plugin instance (as you did in your example), get_access_token_p() will return the refresh token (in addition to the access_token) as explained here. You should store the refresh token at a safe place. Then you can use it at a later time to refresh the access token (for example, if an api call returns an access token expired error). In that case you can call get_refresh_token_p() with the refresh token that you have already stored as a parameter.

    Here, is an example:

    use feature qw(say);
    use strict;
    use warnings;
    use experimental qw(declared_refs refaliasing signatures);
    use JSON;
    use Mojolicious::Lite -signatures;
    
    {
        my @scopes = ('https://www.googleapis.com/auth/userinfo.email');
        my $cred = read_credentials('credentials.json');
        plugin OAuth2 => {
            providers => {
                google => {
                    # "offline": instructs the Google authorization server to also return a
                    # refresh token the first time the application exchanges an
                    # authorization code for tokens
                    access_type  => 'offline',
                    key          => $cred->{client_id},
                    secret       => $cred->{client_secret},
                }
            }
        };
    
        #  Note that this /login/google end point callback is called in two different cases:
        #   - the first case is when the user accesses the login page,
        #   - the second case is when the google authorization server redirects back
        #     to the redirect_uri parameter. In this case the "code" query parameter is
        #     set.
        #  The OAuth2 plugin can differentiate between these two cases, such that
        #    get_token_p() will behave correctly for each case..
        #
        get '/login/google' => sub {
            my $c = shift;
            my $app = $c->app;
            my $args = {
                redirect     => 1, # tell get_token_p() to redirect to the current route
                scope        => (join ' ', @scopes),
            };
            $c->oauth2->get_token_p(google => $args)->then(
                # This callback is for the second response from google aut. server,
                #  (the first response is handled by get_token_p() internally)
                sub {
                    my $res = shift;   # The response from the authorization server
                    $c->session(token => $res->{access_token});
                    $c->session(refresh_token => $res->{refresh_token});
                    #------------------------------------------
                    # This should log the refresh token
                    #------------------------------------------
                    $c->log->info('Refresh token: ' . $res->{refresh_token});
                    #------------------------------------------
    
    
                    my $ua = $app->ua;
                    my $url = 'https://www.googleapis.com/userinfo/v2/me';
                    my $tx = $ua->build_tx(GET => $url);
                    $tx->req->headers->authorization('Bearer ' . $c->session('token'));
                    return $ua->start_p($tx);
                }
            )->then(
                sub {
                    my $tx = shift;
                    my $data = $tx->res->json;
                    #$app->log->info($app->dumper($data));
                    $c->session(user_email => $data->{email});
                    $c->redirect_to('/done');
                }
            )->catch(
                sub {
                    my $err = shift;
                    $c->log->info($err);
                    $c->render(text => $err);
                }
            );
        };
        get '/google/refreshtoken' => sub {
            my $c = shift;
            my $app = $c->app;
            my $refresh_token = $c->session('refresh_token');
            if ($refresh_token) {
                my $args = {
                    refresh_token => $refresh_token,
                };
                $c->oauth2->get_refresh_token_p(google => $args)->then(
                    sub {
                        my $res = shift;
                        # update stored access token
                        $c->session(token => $res->{access_token});
                        $c->render(template => 'refreshed');
                    }
                );
            }
            else {
                $c->render(text => "No refresh token stored. Please login first");
            }
        };
        get '/' => sub {
            my $c = shift;
            $c->render(template => 'index');
        };
        get '/done' => sub {
            my $c = shift;
            $c->render(template => 'done');
        };
        app->start;
    }
    
    sub read_credentials( $fn ) {
        open ( my $fh, '<', $fn ) or die "Could not open file '$fn': $!";
        my $str = do { local $/; <$fh> };
        close $fh;
        my $cred = decode_json( $str );
        return $cred->{installed};
    }
    
    __DATA__
    
    @@ index.html.ep
    <!DOCTYPE html>
    <html>
      <head><title>Testing mojolicious oauth2 refresh token...</title></head>
      <body>
        <h1>Please access route /login/google to start...</h1>
      </body>
    </html>
    
    @@ done.html.ep
    <!DOCTYPE html>
    <html>
      <head><title>Done testing mojolicious oauth2</title></head>
      <body>
        <h1>Done testing. User email: <%= $c->session('user_email') %></h1>
      </body>
    </html>
    
    @@ refreshed.html.ep
    <!DOCTYPE html>
    <html>
      <head><title>Refreshed token</title></head>
      <body>
        <h1>Refreshed token</h1>
      </body>
    </html>