Developers Club geek daily blog

2 years ago
Part one: Google Cloud Endpoints on Java: Manual. p.1

In the first part we considered creation of the project on Google Cloud Endpoints with Java, in this article it will be a question of creation of a frontend to our API.

In addition to the tools used in the first part it is required to us:

AngularJS, and initial general idea about that as it works, optionally Bootstrap or Foundation.

The elementary Web server by the local machine for testing, and the server for deploya applications.

Local server


The application on AngularJS consists of a normal set of "static" files: .html, .js, .css + image files, fonts, etc. It would seem as this usually one-page appendix, it is possible just to open index.html in the browser. However in attempt simply to open index.html in Google Chrome we can see the page without the main contents and in the console something like it:

image

In Firefox the same application can work without Web server, but the link of a type src="//… or href=" … turn into file:///…
Well and Chrome DevTools as my taste is much more convenient.

Therefore it will be more preferable to use the Web server by the local machine nevertheless. It is possible to use Sinatra on Ruby, NodeJS, old kind Apache HTTP Server ("httpd"). App Engine Java SDK also includes development web server started by the local machine, and imitating services of the GAE service including the database (datastore).
In IntelliJ IDEA when editing .html of the file icons of browsers are highlighted if to click that the file will open on the local Web server the started IntelliJ IDEA.

In my opinion, the simplest and universal option is Python (especially as it at you most likely is already set):
python -m SimpleHTTPServer <port>

in this case "python" means start of Python of the version 2.x (by default), "-m" — start of the module, "SimpleHTTPServer" — actually the module the representing elementary http-server and entering standard installation (i.e. if you set Python, this module is already present), optionally — port number if not to specify this parameter, by default there will be 8000.
This command should be performed in a project directory, there where there is our index.htm which will be given by default on the Web server, and on localhost:8000/(http://127.0.0.1:8000/) the static http-server is received. Conveniently to those that any settings in the directory, and in general more no settings are necessary.

Warm


It is possible to place a frontend a web application for our API on any Web server on which it is possible to place static files, for example on free GitHub Pages (see also Setting up a custom domain with GitHub Pages) or on Amazon Web Services (AWS).

Naturally logical solution is also placement of a frontend on GAE. It can be in one project. In our manual, in the educational purposes, we will place a backend and a frontend in different projects on GAE. In the previous part we created for this purpose two projects in the developer's console. Frontend will live the project in hello-habrahabr-webapp, and a backend in hello-habrahabr-api.

Domain


In addition to the available domain name of a type {ID project} of .appspot.com, we can use own domain registered both through Google Domains and at other logger, or several domains for one project. Earlier this opportunity was available only to the domains used in Google Apps, but since recent time the binding to Google Apps is cleaned.

To add the domain, we select the project in the console of the developer and we pass into the menu: Compute-> App Engine-> Settings-> Custom domains. Then we select 'Register a new domain' to register the domain at Google or if we already have an Add a custom domain domain

image

If we add the domain registered not at Google, then at first it is necessary to undergo testing that you the owner of the domain and to add the domain to the list checked on for your account by Google: we enter domain name in the field designated by digit 1, we click 'Verify' and we pass to the page of verification:

image

if the domain is registered at godaddy.com or another large to for verification it is necessary to select provider and and the opened window to log in:

image

and to confirm:

image

Or to make verification manually, instructions will be shown by clicking on 'Add a TXT record.'

On the page:

image

it is possible to add "property" of the verified domains, for example to add the additional account of Google with which it is possible to manage the domain in Google services.

After verification of the domain, in the section designated by digit 2. (if we just added the new domain, then it is necessary to overload the page that the new domain became available to the choice) we can appropriate this domain to the current project, or "entirely", or to create and appropriate the subdomain, or both.

image

After the choice of the domain and clicking of the Add button, it is necessary to make changes to settings of the domain at the logger (DNS Zone File) according to the instructions specified in the section under digit 3:

image

Attention: after clicking of the Add button visually nothing is reflected, it is necessary to pass into other section (for example 'Application settings'), and again to return to 'Custom domains' — now the added domains and their settings have to be shown there.

Now the project will be (not at once, and during time till 24 o'clock) is available both to the address {ID project} .appspot.com, and to the added addresses (domains).

SSL setup


Access to the address {ID project} .appspot.com can be provided as on http and https
To provide access on https to own domain, we will need the SSL certificate by the signed certifying center (English Certification authority, CA) which signature is accepted by browsers.
The leading providers of services of certification center are Symantec with the brands GeoTrust, Thawte, Verisign, the same Godaddy, and Comodo now. The greatest share of the market, and lower prices at Comodo. If to purchase certificates through resellers the price is normal even below.

For beginning/training it is possible to use the free certificate from Comodo for 90 days (once on the domain is free of charge issued) and/or the similar sentence for 30 days from RapidSSL.

To receive the certificate it is necessary to send to certificate signing request (CSR) provider (request for the signature of the certificate). Some services offer generation of the certificate on the website, however in that case there is no guarantee that only you therefore it is better for CSR to generate independently will have a secret key. For this purpose we will use the utility of OpenSSL.

At first we will create the file of a configuration for CSR (we will call it scr.conf):
# csr.conf
# you can rename scr.conf, scr.key, scr.csr to filenames you prefer,
# but with the same filename extensions
[req]
default_bits       = 2048 #
default_md         = sha512
default_keyfile    = csr.key #name of keyfile
distinguished_name = req_distinguished_name
prompt             = yes
# If 'prompt = no' provide right values to properties, not to _default properties
# and remove or comment _default properties.
# Use 'yes' and _default to see and correct values interactively
encrypt_key        = no #
# req_extensions = v3_req           #this is for multi-domain certificate

[req_distinguished_name]
#
# Use your company name, e-mai, domain names as default values
# if you enter '.', the field will be left blank
#
countryName = Country Name (2 letter code)
countryName_default = GB
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = City of London
localityName = Locality Name (eg, city)
localityName_default = London
organizationName = Organization name
organizationName_default = MyCompany Limited
organizationalUnitName = Organizational Unit Name (eg, section)
organizationalUnitName_default = .
commonName = This is fully qualified domain name that you wish to secure
# e.g. www.example.com or mail.example.com
commonName_default = www.my-domain.com
emailAddress = Email Address
emailAddress_default = admin@my-domain.com

# [v3_req]
# subjectAltName = @alt_names       #this is for multi-domain certificate

# [alt_names]                       #this is for multi-domain certificate
# DNS.1   = my-domain.net           #this is for multi-domain certificate
# DNS.2   = my-domain.org           #this is for multi-domain certificate
# DNS.3   = myseconddomain.com      #this is for multi-domain certificate

# to generate .csr:
# openssl req -newkey rsa:2048 -sha512 -out csr.csr -config csr.conf
# or:
# openssl req -newkey rsa:2048 -sha512 -nodes -out csr.csr -config csr.conf
# Note:  If the "-nodes" is entered the key will NOT be encrypted with a
# DES pass phrase, ( see:
# https://support.comodo.com/index.php?/Default/Knowledgebase/Article/View/1/19/csr-generation-using-openssl-apache-wmod_ssl-nginx-os-x
# )
#
# to verify .csr:
# openssl req -text -noout -verify -in csr.csr
#
# in one command generate and verify:
# openssl req -newkey rsa:2048 -out csr.csr -config csr.conf &&openssl req -text -noout -verify -in csr.csr
#



now we will start command:

openssl req -newkey rsa:2048 -out csr.csr -config csr.conf


if we specified in settings 'prompt = by yes', then we have an opportunity to browse the entered values and to change them. If it is not necessary to change the values specified by default, then just we press 'Enter'

As a result we will create two new files: csr.key is a private key, and csr.csr is CSR (request for the signature of the certificate)

it is possible to check CSR command:
openssl req -text -noout -verify -in csr.csr

The .csr file needs to be opened a text editor, inside there will be something it seems:
-----BEGIN CERTIFICATE REQUEST-----
MIIC5TCCAc0CAQAwgZ8xCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5DaXR5IG9mIExv
bmRvbjEPMA0GA1UEBwwGTG9uZG9uMRowGAYDVQQKDBFNeUNvbXBhbnkgTGltaXRl
NZBB4bDdgJ+uyNZq54dM1tUvzSolv/+LAY78/z85edqLH4nc5CxgMEn8hurFOpB4
RXS+ShhpBsJr6RJhSk2xkRe/idkM/TUon/7n1TUthFpjv2tYQZ6on3iWUZ61FDuM
mNPHGMIX+sn/OceViRtlu1Lx+t4JV9dTJQ==
-----END CERTIFICATE REQUEST-----

The contents of the file will need to be copied and to insert into a form on the website, approximately such:

image

For the simplest check of ownership of the domain you have to have an access to e-mail which issues whois, or to one of email addresses in this domain:
admin@
administrator@
postmaster@
hostmaster@
webmaster@

Or it is necessary to place http://yourdomain.com/{ Upper case value of MD5 hash of CSR } .txt or to modify DNS CNAME record having specified besides the corresponding hash.

Expanded check: Extended Business Verification or in abbreviated form EV, demands providing documents of the organization on which the certificate is issued, but allows to receive the certificate with "a green line" (GreenBar) the name of the organization — the certificate holder is reflected in the browser in which.

After check passing you will be able to unload from the website of provider or to receive the file of a type { your domain or number of the order } .crt by e-mail, is normal in a packet (.zip) with certificates of the certified center which also have the .crt expansion This .zip file it is necessary to extract in a separate directory.

Our certificate we can check and browse information which is contained in it command:
openssl x509 -in {имя файла}.crt -text -noout

where .crt {file name} — a name of our file of the certificate.

Now for GAE we need to convert our private key from the .key format into the .pem format:
openssl rsa -in *.key > forGAE.key.pem


Attention: it is not necessary to specify parameter in this command - text as recommend on the website of Google as this command adds the unnecessary and superfluous in this case text to a key. If in attempt to load a key, you receive the message:
'The private key you've selected does not appear to be valid'
that most likely the reason in it. Open the file a text editor and delete everything that is higher than a line-----BEGIN RSA PRIVATE KEY-----
Or convert a key once again, without parameter - text

2) to connect (concatenate) all .crt files from the received file .zip (both our certificate, and certificates of the server), command of such type:
cat mydomain_com.crt ASecureServerCA.crt ATrustCA.crt ATrustExternal.crt > concat.crt

Attention: the order of files is important, (cat *.crt is simple> concat.crt in this case will not approach) our received certificate of our domain, then certificates of the certified center has to go the first names of falls of which will differ, but there will be similar not those that in an example.

Now we will check the received files:

openssl x509 -noout -modulus -in concat.crt | openssl md5

and
openssl rsa -noout -modulus -in forGAE.key.pem | openssl md5


have to issue identical value.

Now we pass Compute into the menu of the console of the developer-> Settings-> SSL Sertificates-> Upload a new certificate

image

In the field of 'Name' we enter a name which we want to give to our certificate in the developer's console.
We load our concat.crt into 'PEM encoded X.509 public key certificate'
In the field of 'Unencrypted PEM encoded RSA private key' we load our key in the .pem (forGAE.key.pem) format
We press 'Upload'

image

We note checkboxes opposite to domain names for which we can (and we want) to activate the certificate (are those which are specified in the certificate, it is natural), and we press the blue Save button:

image

Now in an address bar of the browser we will gather a name of our domain having entered at the beginning the httpS: protocol//

image

On the website there is nothing yet, but at the beginning of an address bar we will see a green lock, having clicked it we can browse data of the certificate.
SSL works.

Application skeleton


Also as in the first part we use Maven:
mvn archetype:generate -Dappengine-version=1.9.28 -Dapplication-id=hello-habrahabr-webapp -Dfilter=com.google.appengine.archetypes:

(hello-habrahabr-webapp — in this case is the ID project)

But as in this project there will be no Cloud Points API, instead of an archetype No. 2, select an archetype No. 1 (a basic skeleton of the application on GAE):
1: remote -> com.google.appengine.archetypes:appengine-skeleton-archetype (A skeleton application with Google App Engine)

the rest — similar to what is stated in the first part, but with another the ID project.
Thus Maven will create to us the hello-habrahabr-webapp folder, with a file structure similar to the previous project, but in this case the directory of src/main/java/will be empty, and for a frontend it will not be required to us now.

Similarly as we did we edit earlier pom.xml (in 1.9.27 it is possible to replace with the last 1.2.8 now). We pass into a directory of src/main/webapp/WEB-INF/we edit the appengine-web.xml and web.xml files
web.xml us is almost empty now therefore we will add to it (between) the sections security-constraint and welcome-file-list:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <!-- Force SSL for entire site -->
    <!-- (server automatically redirects the requests to the SSL port
        if you try to use HTTP ) -->
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Entire Application</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
    <!-- Welcome file list -->
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
    <!--  -->
</web-app>


We create in this project scripts of set.account.sh and commit.push.build.and.upload.sh similar to that which were in the first part, and we start commands:
git init
gcloud init
git config credential.helper gcloud.sh
git remote add google https://source.developers.google.com/p/hello-habrahabr-webapp
git push --all google
set.account.sh
commit.push.build.and.upload.sh

We are convinced that the project gathers successfully and loaded on the server.

Maven collects the project in .war the file which is in target directory and unloads it on the GAE server. The same file can be deploit also on other compatible java-server (Tomcat, Jetty)

Also we can be convinced that in attempt to visit the website under the http:// protocol the server automatically transports on https://

Access to API without authentication


Let's begin with simple. Last time we created simple API available without authentication ('myApi API') on POST to request for the address hello-habrahabr-api.appspot.com/_ah/api/myApi/v1/register (YourFirstAPI.java file). Let's modify several this file so that it issued information in logs which I remind are available in the developer's console in the menu of the project: Monitoring-> Logs, also contained also a method the request processing GET:
package com.appspot.hello_habrahabr_api;

import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiMethod.HttpMethod;
import java.util.logging.Logger;
import java.util.Random;

@Api(name = "myApi", //The api name must match '[a-z]+[A-Za-z0-9]*'
     version = "v1",
     scopes = {Constants.EMAIL_SCOPE},
     description = "first API for this application.")
public class YourFirstAPI {

  @SuppressWarnings("unused")
  private static final Logger LOG = Logger.getLogger(YourFirstAPI.class.getName());

  @ApiMethod(
             name = "registerPOST",
             path = "register",
             httpMethod = HttpMethod.POST
             )
  @SuppressWarnings("unused")
  public MessageToUser registerPOST(final UserForm userForm) {

    LOG.warning("[YourFirstAPI] (HTTP Method: POST): userForm: {name: " + userForm.getName() + ", age: " + userForm.getAge() + ", ishuman: " + userForm.getIshuman() + "}" );

    MessageToUser messageToUser = new MessageToUser();
    messageToUser.setMessage("Hi, " + userForm.getName() + ", you are registered on our site");
    Random random = new Random();
    messageToUser.setUsernumber(random.nextInt(100) + 1);
    messageToUser.setIsregistered(true);
    return messageToUser;}

  @ApiMethod(
             name = "getGreeting",
             path = "getgreeting",
             httpMethod = HttpMethod.GET
             )
  @SuppressWarnings("unused")
  public MessageToUser getGreeting(final UserForm userForm) {

    LOG.warning("[YourFirstAPI] (HTTP Method: GET): userForm: {name: " + userForm.getName() + ", age: " + userForm.getAge() + ", ishuman: " + userForm.getIshuman() + "}" );

    MessageToUser messageToUser = new MessageToUser();

    messageToUser.setMessage("Hi, " + userForm.getName() + ", nice to meet you :)");
    Random random = new Random();
    messageToUser.setUsernumber(random.nextInt(100) + 1);
    messageToUser.setIsregistered(false);
    return messageToUser;}
}


and zadeploy on the server (we will use earlier created commit.push.build.and.upload.sh script)

Now in the project in which we create a frontend we will create a form for such request. Let's pass into src/main/webapp/directory - it is the place where it is possible to locate static files, since index.html, a directory, except WEB-INF/will also be available to requests. At the same time as after all the Java-server that of course any such file can as well to be generated by a servlet.

So, index.html:
<!DOCTYPE html>
<html lang="en">

<head>

    <meta charset="utf-8">
    <link rel="shortcut icon" href="favicon.ico?" />

    <!-- JQUERY 2.1.4 -->
    <script src="vendors/jquery-2.1.4.js"></script>
    <!-- BOOTSTRAP 3.3.5 -->
    <link rel="stylesheet" href="vendors/bootstrap.css">
    <link rel="stylesheet" href="vendors/bootstrap-theme.css">
    <script src="vendors/bootstrap.js"></script>
    <!-- ANGULARJS 1.4.7 -->
    <script src="vendors/angular.js"></script>
    <script src="vendors/angular-route.js"></script>
    <!--
    <script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
    <script src="https://code.angularjs.org/1.4.7/angular-route.min.js"></script>
    -->

    <!-- angular-google-gapi -->
    <!-- https://github.com/maximepvrt/angular-google-gapi/releases -->
    <script src="vendors/angular-google-gapi.js"></script>
    <!-- ngProgress -->
    <!-- https://github.com/VictorBjelkholm/ngProgress -->
    <script src="vendors/ngprogress.js"></script>
    <link rel="stylesheet" href="vendors/ngProgress.css">
    <!-- angular-google-gapi -->
    <!-- https://github.com/maximepvrt/angular-google-gapi/releases -->
    <script src="vendors/angular-google-gapi.js"></script>

    <!--  ~~~ MY APP ~~~ -->
    <!-- main app module -->
    <script src="js/app.js"></script>
    <!-- controllers -->
    <script src="js/simpleformcontroller.js"></script>
    <script src="js/authformcontroller.js"></script>
    <!-- -->
    <title>Cloud Endpoints Frontend</title>

</head>

<body ng-app="myApp">

    <ng-view></ng-view>

</body>

</html>



We prescribed AngularJS with the module (from a standard set) ngRoute.

And also, for that the website had friendly to the user an appearance, we will add Bootstrap (+jQuery), and AngularJS the ngProgress module. The last serves for display of indicators of process (progress bar), we will use it to show to the user the course of execution of requests.

We designated an angualrjs-application as "myApp" () and specified to the application the place for an insert of templates (), and two angularjs-kotroller specified: js/simpleformcontroller.js and js/authformcontroller.js, respectively for work with requests with authentication and without it.

The angular-google-gapi module will be required to us for work with authentication, and will be considered further.

Now we will create the main app.js module:
'use strict';

(
    function () {
        // create main module
        var app = angular.module("myApp", [
                'ngRoute',   // https://code.angularjs.org/1.4.7/docs/api/ngRoute
                'ngProgress' // https://github.com/victorbjelkholm/ngprogress
            ]
        );
        // routes
        app.config(function ($routeProvider) {
                $routeProvider.when('/', {
                    templateUrl: "templates/simpleform.html",
                    controller: "SimpleFormController"
                }).when('/auth/', {
                        templateUrl: "templates/authform.html",
                        controller: "AuthFormController"
                    })
                    .otherwise({redirectTo: '/'})
            }
        );
    }()
);



And js/simpleformcontroller.js controller:
'use strict';

(
    function() {

        var SimpleFormController = function($scope, $http, ngProgressFactory) {

            $scope.progressbar = ngProgressFactory.createInstance();
            $scope.progressbar.setColor('blue');

            $scope.data = {}; // data from form (ng-model="data. ...")

            // $http configuration object
            // https://code.angularjs.org/1.4.7/docs/api/ng/service/$http#usage
            $scope.postReq = {
                method: 'POST',
                url: 'https://hello-habrahabr-api.appspot.com/_ah/api/myApi/v1/register',
                data: $scope.data // 'data' for POST
            };

            $scope.getReq = {
                method: 'GET',
                url: 'https://hello-habrahabr-api.appspot.com/_ah/api/myApi/v1/getgreeting',
                params: $scope.data // 'params:' for GET
            };

            $scope.actionPost = function() {
                $scope.progressbar.start();
                console.log("request sent:"); // for testing
                console.log($scope.postReq); // for testing
                // AJAX request:
                // $http(req).then(function(){...}, function(){...});
                $http($scope.postReq).then(function(response) {
                        // success:
                        $scope.progressbar.complete();
                        $scope.serverresponse = response;
                        $scope.errormessage = null;
                        console.log("response received:");
                        console.log(response);
                    }, // comma operator, see: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Comma_Operator
                    function(response) {
                        // error:
                        $scope.progressbar.complete();
                        $scope.errormessage = response;
                        $scope.serverresponse = null;
                        console.log("get Error form server:");
                        console.log(response);
                    });
            };

            $scope.actionGet = function() {
                $scope.progressbar.start();
                console.log("request sent:"); // for testing
                console.log($scope.getReq); // for testing
                // AJAX request:
                // $http(req).then(function(){...}, function(){...});
                $http($scope.getReq).then(function(response) {
                        // success:
                        $scope.progressbar.complete();
                        $scope.serverresponse = response;
                        $scope.errormessage = null;
                        console.log("response received:");
                        console.log(response);
                    }, // comma operator, see: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Comma_Operator
                    function(response) {
                        // error:
                        $scope.progressbar.complete();
                        $scope.errormessage = response;
                        $scope.serverresponse = null;
                        console.log("get Error form server:");
                        console.log(response);
                    });
            };

            $scope.clear = function() {
                $scope.serverresponse = null;
                $scope.errormessage = null;
                $scope.data = null;
            };
        };

        // $inject property annotation
        // see: https://code.angularjs.org/1.4.7/docs/guide/di
        SimpleFormController.$inject = ['$scope', '$http', 'ngProgressFactory'];

        angular.module('myApp')
            .controller('SimpleFormController', SimpleFormController);

    }());



So: we created the MyApp module, the SimpleFormController controller and in it In this case data loading from the server we will do functions for GET and POST of requests to 'myApi API' (which are in turn processed by a code in the YourFirstAPI.java file) after clicking of buttons, but of course it is possible to load data and at once when loading the controller.

Now we create the templates/simpleform.html template in which there will be a HTML form for request to API and the answer of the server will be displayed:

<div class="container">
    <!-- Navigation Bar  -->
    <nav class="navbar navbar-inverse">
        <a class="navbar-brand" href="#/">Front-end for Cloud Endpoints API</a>
        <ul class="nav navbar-nav">
            <li class="active"><a href="#/">Home</a></li>
            <li><a href="#/auth/">Auth</a></li>
        </ul>
    </nav>
    <!-- Form -->
    <form>
        <fieldset>
            <legend style="font-weight: bold;">Submit your data to server</legend>
            <p>
                <label> Name:
                    <input ng-model="data.name">
                </label>
            </p>
            <label> Age:
                <p>
                    <input type="range" min="0" max="120" step="1" ng-model="data.age">
                </p>
                <p>
                    <input ng-model="data.age" name="ageNumber" class="col-md-3">
                    <span class="col-md-5">years old</span>
                </p>
            </label>
            <br>
            <label>
                <p> Is a human:
                    <input type="checkbox" ng-model="data.ishuman" name="data.ishuman">
                </p>
            </label>
        </fieldset>
        <input type="button" value="Submit GET" ng-click="actionGet()">
        <input type="button" value="Submit POST" ng-click="actionPost()">
        <input type="reset" ng-click="clear()">
    </form>
    <!-- Server Response -->
    <br>
    <div class="panel panel-default">
        <div ng-hide="(serverresponse != null || errormessage != null)" class="panel-body">
            <p></p>
            <br>
            <p></p>
            <br>
        </div>
        <div ng-show="serverresponse != null" class="panel-body">
            <p>Response from server: </p>
            <p>
                {{ serverresponse.data.message }} and your number is: {{serverresponse.data.usernumber}}
            </p>
        </div>
        <div ng-show="errormessage != null" class="panel-body">
            <p>Error from server: </p>
            <p>
                {{errormessage.data.error.message}}
            </p>
        </div>
    </div>
</div>



Total at this stage: index.html, templates/simpleform.html, js/app.js, js/simpleformcontroller.js

For check we start the server in src/main/webapp/directory:
python -m SimpleHTTPServer

also we test the application:

image

image

If everything is normal — deploy.

Access to API with authentication with use of login password of the account of Google (OAuth 2.0)


Preparation of a backend


At the beginning in the developer's console in a project backend (we it had hello-habrahabr-api) in the menu:
APIs &auth-;> Credentials-> Add Credentials-> OAuth 2.0 Client ID
we have to register addresses of domains from which Javascript can address API (Authorized JavaScript origins) using OAuth 2.0 from Google, i.e. we will prescribe in our case to hello-habrahabr-webapp.appspot.com and/or the domain on which we made SSL certificates as it was discussed above. If an opportunity to log in from the local server is necessary, then it is necessary to prescribe localhost + port number, for example localhost:8000

In the field of 'Name' we enter any name of a configuration, for example 'Web Client', and we save a configuration the Save button:

image

It is necessary to copy contents of the Client ID field, a type: 647700180043-m8et0au4vhgiv2n4iqr2hssn0mkkl7q0.apps.googleusercontent.com or to load the JSON file with all configuration settings having clicked the icon which on the picture is noted red.

In the project with API (at us it is hello-habrahabr-api) in the Constants.java file: WEB_CLIENT_ID needs to appropriate Client ID value, and EMAIL_SCOPE — "www.googleapis.com/auth/userinfo.email":
package com.appspot.hello_habrahabr_api;

import com.google.api.server.spi.Constant;

/**
 * Contains the client IDs and scopes for allowed clients consuming your API.
 */
public class Constants {
    public static final String WEB_CLIENT_ID = "647700180043-m8et0au4vhgiv2n4iqr2hssn0mkkl7q0.apps.googleusercontent.com";
    public static final String ANDROID_CLIENT_ID = "replace this with your Android client ID";
    public static final String IOS_CLIENT_ID = "replace this with your iOS client ID";
    public static final String ANDROID_AUDIENCE = WEB_CLIENT_ID;

    public static final String EMAIL_SCOPE = "https://www.googleapis.com/auth/userinfo.email";

    public static final String API_EXPLORER_CLIENT_ID = Constant.API_EXPLORER_CLIENT_ID;
}

Now we modify our OAuth2Api.java, it is necessary to state in the summary of Api:
        scopes = {Constants.EMAIL_SCOPE},
        clientIds = {Constants.WEB_CLIENT_ID, Constants.API_EXPLORER_CLIENT_ID},

It means that this class will process requests from web clients to whom the specified Client ID (at the same time the client is appropriated not only has to transfer correct Client ID, but also the request has to be from the domain of APIs &auth; specified in a configuration — Credentials) with authentication with use of email. If these parameters are not specified in OAuth2Api.java, then will respond to the requests of API with an error 401. Respectively, if not to specify Constants.API_EXPLORER_CLIENT_ID, then when testing in API Explorer, i.e. the web interface to the address of a type {ID project} .appspot.com / _ ah/api/explorer an error message 401 will also be given (that is the server cannot make user authentication)

OAuth2Api.java in the new edition:
package com.appspot.hello_habrahabr_api;

/**
 * explore on: https://apis-explorer.appspot.com/apis-explorer/?base=https://hello-habrahabr-api.appspot.com/_ah/api#p/oAuth2Api/v1/
 */

import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiMethod.HttpMethod;
import com.google.api.server.spi.response.UnauthorizedException;
import com.google.appengine.api.users.User;
import com.google.appengine.repackaged.com.google.gson.Gson;

import java.util.Random;
import java.util.logging.Logger;

@Api(name = "oAuth2Api", // The api name must match '[a-z]+[A-Za-z0-9]*'
        version = "v1",
        scopes = {Constants.EMAIL_SCOPE},
        clientIds = {Constants.WEB_CLIENT_ID, Constants.API_EXPLORER_CLIENT_ID},
        description = "API using OAuth2")
public class OAuth2Api {

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger.getLogger(OAuth2Api.class.getName());

    @ApiMethod(
            name = "getUserInfo",
            path = "getuserinfo",
            httpMethod = HttpMethod.POST
    )
    @SuppressWarnings("unused")
    public MessageToUser getUserInfo(final User user, final UserForm userForm)
            throws UnauthorizedException {

        if (user == null) {
            LOG.warning("User not logged in");
            throw new UnauthorizedException("Authorization required");
        }

        MessageToUser messageToUser = new MessageToUser();
        Gson gson = new Gson();
        messageToUser.setMessage(
                "Hi, " +
                        userForm.getName() +
                        ", your data from Google: " +
                        gson.toJson(user)
        );
        Random random = new Random();
        messageToUser.setUsernumber(random.nextInt(100) + 1);
        messageToUser.setIsregistered(true);

        return messageToUser;
    }
}


We can test our API in in API Explorer, and we will pass to a frontend.

Frontend for requests with authentication


With authentication of Google provides API Client Library for JavaScript library for access to API, for our application we will use the angular-google-gapi module which in fact represents a wrapper for the specified library on the web client of AngularJS and does its use with AngularJS much more convenient. Basic instructions for use of the module see on github.com/maximepvrt/angular-google-gapi

We added
<script src="vendors/angular-google-gapi.js"></script> 

in index.html

It is necessary to add dependence to the main application module (var app = angular.module ('myApp', ['angular-google-gapi']);), and to set a configuration by means of app.run (…).
It will look so (js/app.js):
'use strict';

(
    function () {

        // create main module
        var app = angular.module("myApp", [
            'ngRoute', // https://code.angularjs.org/1.4.7/docs/api/ngRoute
            'ngProgress', // https://github.com/victorbjelkholm/ngprogress
            'angular-google-gapi' // https://github.com/maximepvrt/angular-google-gapi/ - add app.run() after app.config()
        ]);

        // routes
        app.config(function ($routeProvider) {
            $routeProvider.when('/', {
                templateUrl: "templates/simpleform.html",
                controller: "SimpleFormController"
            }).when('/auth/', {
                templateUrl: "templates/authform.html",
                controller: "AuthFormController"
            }).otherwise({redirectTo: '/'})
        });

        app.run(['GAuth', 'GApi', 'GData', '$rootScope',
            function (GAuth,
                      GApi,
                      GData,
                      $rootScope) {
                $rootScope.gdata = GData; //

                var CLIENT = '647700180043-m8et0au4vhgiv2n4iqr2hssn0mkkl7q0.apps.googleusercontent.com';
                var BASE = 'https://hello-habrahabr-api.appspot.com/_ah/api';
                GApi.load('oAuth2Api', 'v1', BASE);
                GAuth.setClient(CLIENT);

                // see: https://github.com/maximepvrt/angular-google-gapi/issues/8
                GAuth.setScope("https://www.googleapis.com/auth/userinfo.email");

                GAuth.checkAuth().then(
                    function () {
                        // action if it's possible to authenticate user at startup of the application
                        console.log("user authenticated, $rootScope.gapi.user: ");
                        console.log(console.log($rootScope.gapi.user));
                    },
                    function () {
                        // action if it's impossible to authenticate user at startup of the application
                        console.log("user not authenticated, $rootScope.gapi.user: ");
                        console.log(console.log($rootScope.gapi.user));
                    }
                );

                $rootScope.login = function () { // shows auth window from Google
                    GAuth.login().then(
                        function () {
                            console.log('user logged in');
                            console.log(console.log($rootScope.gapi.user));
                        });
                };

                /*
                 *  As stated on
                 *  https://cloud.google.com/appengine/docs/java/endpoints/consume_js:
                 *  "There is no concept of signing out using the JS client library,
                 *  but you can emulate the behavior if you wish by any of these methods:
                 *  - Resetting the UI to the pre signed-in state.
                 *  - Setting any signed-in variables in your application logic to false
                 *  - Calling gapi.auth.setToken(null);" // <- *
                 * */
                $rootScope.logout = function () {
                    GAuth.logout().then(
                        function () {
                            console.log('user logged out');
                            console.log(console.log($rootScope.gapi.user));
                        });
                };
            }
        ]);
    }()
);



Pay attention that the login functions () and logout () we placed in' $rootScope', in our example it not essentially, but thus they will be available in different parts of the application (when there are these different parts)

Now js/authformcontroller.js controller:
'use strict';

(
    function () {

        var AuthFormController = function ($scope, $rootScope, $http, GApi, GAuth, ngProgressFactory) {

            $scope.progressbar = ngProgressFactory.createInstance();
            $scope.progressbar.setColor('#00F');

            $scope.data = {}; // object to store form data

            $scope.authrequest = function () {

                $scope.progressbar.start();
                GAuth.checkAuth().then(
                    function () {
                        // if it's possible to authenticate user
                        GApi.executeAuth( // execute request
                            'oAuth2Api',
                            'getUserInfo',
                            $scope.data // data to send to server
                            )
                            .then(function (resp) {  // receive response
                                $scope.userinfofromserver = resp;
                                console.log("GApi.executeAuth() get resp:");
                                console.log(resp);
                                $scope.progressbar.complete();
                            });
                    },
                    function () {
                        // if it's impossible to authenticate user
                        $scope.userinfofromserver = {};
                        $scope.userinfofromserver.message = 'User not logged in ';
                        console.log("GAuth.checkAuth() - failed: ");
                        console.log(console.log($rootScope.gapi.user));
                        $scope.progressbar.complete();
                    }
                );
            };

            $scope.clear = function () {
                $scope.userinfofromserver = null;
                $scope.data = null;
            };

        };

        // $inject property annotation
        // see: https://code.angularjs.org/1.4.7/docs/guide/di
        AuthFormController.$inject = ['$scope', '$rootScope', '$http', 'GApi', 'GAuth', 'ngProgressFactory'];

        angular.module('myApp')
            .controller('AuthFormController', AuthFormController);

    }()

);



Pay attention that before starting GApi.executeAuth (), we start GAuth.checkAuth () — it differs from approach of the module used by the author, but it seems to me it is more logical, GApi.executeAuth () — will update authentication before sending request, thus the user will not receive an authorization error at request after the long period of failure to act on the website, etc.

$rootScope at us will contain now object of gapi.user which contains available information on the logged-in user:
  • user.email
  • user.picture (an avatar of the user, url on the picture)
  • user.id (Google id (number))
  • user.name (a user name or if it is absent, then email)
  • user.link (the link to the page of the user of Google+ if it exists)

Thus in a template we can show to the logged-in user its photo and email, we will use it in templates/authform.html:
<div class="container">
    <!-- Navigation Bar  -->
    <nav class="navbar navbar-inverse">
        <a class="navbar-brand" href="#/">Front-end for Cloud Endpoints API</a>
        <ul class="nav navbar-nav">
            <li><a href="#/">Home</a></li>
            <li class="active"><a href="#/auth/">Auth</a></li>
        </ul>
        <!--  -->
        <ul class="nav navbar-nav navbar-right">
            <li>
                <a ng-show="gapi.user" href="" class="navbar-brand" style="padding-top: 0;">
                    <img src="{{gapi.user.picture}}" class="navbar-brand"
                         style="padding-top: 0; padding-bottom: 0;"></a>
            </li>
        </ul>
        <!--  -->
        <ul class="nav navbar-nav navbar-right">
            <!--  -->
            <li ng-show="!gapi.user" class="active">
                <a href="" ng-click="login()">login</a>
            </li>
            <!--  -->
            <li ng-show="gapi.user">
                <a href="">{{gapi.user.email}}</a>
            </li>
            <li ng-show="gapi.user">
                <a href="" ng-click="logout()">logout</a>
            </li>
        </ul>
    </nav>
    <!-- Form -->
    <form>
        <fieldset>
            <legend style="font-weight: bold;">Submit your request to server</legend>
            <p>
                <label> Your name:
                    <input ng-model="data.name">
                </label>
            </p>
        </fieldset>
        <input type="button" value="Auth request" ng-click="authrequest()">
        <input type="reset" ng-click="clear()">
    </form>
    <!-- Server Response -->
    <br>

    <div class="panel panel-default" style="font-weight: bold;" >
        <!--  -->
        <div ng-hide="(userinfofromserver != null)" class="panel-body">
            <p></p>
            <br>

            <p></p>
            <br>
        </div>
        <!--  -->
        <div ng-show="userinfofromserver" class="panel-body"
             style="">
            <p> User's data from server (reg # {{userinfofromserver.usernumber}}): </p>

            <p>{{userinfofromserver.message}}</p>
        </div>
        <!--  -->
    </div>
</div>


Let's overload the home page of our website:

image

In the console we see that the script tried to recognize the user at once, but the user still "unfamiliar".
We follow the link 'Auth':
image
We enter a name in the field of 'Your name' and we press the Auth request button:
image
Now we log in (the link 'login' on the navigation panel), using the account of Google:
image
After login our navigation panel will show the user's photo from the account of Google with which email logged in the user, and the link 'logout'.
If we send now request for the server (Auth request button) again, we will receive the answer of the server as it befits the logged-in user:
image

In such a way we can implement communication of the web client and the server via a secure channel and with authentication of users without need to store passwords on own server. As I it was already told in the previous article, lack of own authentication login password on the websites and use of authentication of OAuth of 2.0 reliable providers will become possible a trend.

Additional materials:



This article is a translation of the original post at habrahabr.ru/post/270459/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus