# :nodoc:
#
# Copyright (C) 2014-2015 Authlete, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'json'
require 'rack'
require 'rest-client'
module Authlete
# == Authlete::Api Module
#
# A web client that accesses Authlete Web APIs.
#
class Api
include Authlete::Utility
# The host which provides Authlete Web APIs.
# For example, https://dev-api.authlete.com
attr_accessor :host
# The API key of a service owner.
attr_accessor :service_owner_api_key
# The API secret of a service owner.
attr_accessor :service_owner_api_secret
# The API key of a service.
attr_accessor :service_api_key
# The API secret of a service.
attr_accessor :service_api_secret
# Extra HTTP headers
attr_accessor :extra_headers
private
# The constructor which takes a hash containing configuration
# parameters. Valid configuration parameter names are as follows.
#
# - :host
# - :service_owner_api_key
# - :service_owner_api_secret
# - :service_api_key
# - :service_api_secret
#
def initialize(config = {})
@host = extract_value(config, :host)
@service_owner_api_key = extract_value(config, :service_owner_api_key)
@service_owner_api_secret = extract_value(config, :service_owner_api_secret)
@service_api_key = extract_value(config, :service_api_key)
@service_api_secret = extract_value(config, :service_api_secret)
end
def call_api(method, path, content_type, payload, user, password)
headers = {}
if content_type.nil? == false
headers.merge!(:content_type => content_type)
end
if @extra_headers.nil? == false
headers.merge!(@extra_headers)
end
response = execute(
:method => method,
:url => @host + path,
:headers => headers,
:payload => payload,
:user => user,
:password => password
)
body = body_as_string(response)
if body.nil?
return nil
end
JSON.parse(response.body.to_s, :symbolize_names => true)
end
def execute(parameters)
begin
return RestClient::Request.new(parameters).execute
rescue => e
raise_api_exception(e)
end
end
def raise_api_exception(exception)
message = exception.message
response = exception.response
if response.nil?
# Raise an error without HTTP response information.
raise Authlete::Exception.new(:message => message)
end
# Raise an error with HTTP response information.
raise_api_exception_with_http_response_info(message, response.code, response.body)
end
def raise_api_exception_with_http_response_info(message, status_code, response_body)
# Parse the response body as a json.
json = parse_response_body(response_body, message, status_code)
# If the json has the HTTP response information from an Authlete API.
if has_authlete_api_response_info(json)
# Raise an error with it.
hash = json.merge!(:statusCode => status_code)
raise Authlete::Exception.new(hash)
end
# Raise an error with 'status_code' and the original error message.
raise Authlete::Exception.new(
:message => message,
:status_code => status_code
)
end
def parse_response_body(response_body, message, status_code)
begin
return JSON.parse(response_body.to_s, :symbolize_names => true)
rescue
# Failed to parse the response body as a json.
raise Authlete::Exception.new(
:message => message,
:status_code => status_code
)
end
end
def has_authlete_api_response_info(json)
json && json.key?(:resultCode) && json.key?(:resultMessage)
end
def body_as_string(response)
if response.body.nil?
return nil
end
body = response.body.to_s
if body.length == 0
return nil
end
return body
end
def call_api_service_owner(method, path, content_type, payload)
call_api(method, path, content_type, payload, @service_owner_api_key, @service_owner_api_secret)
end
def call_api_service(method, path, content_type, payload)
call_api(method, path, content_type, payload, @service_api_key, @service_api_secret)
end
def call_api_json(path, body, user, password)
call_api(:post, path, 'application/json;charset=UTF-8', JSON.generate(body), user, password)
end
def call_api_json_service_owner(path, body)
call_api_json(path, body, @service_owner_api_key, @service_owner_api_secret)
end
def call_api_json_service(path, body)
call_api_json(path, body, @service_api_key, @service_api_secret)
end
def build_error_message(path, exception)
begin
# Use "resultMessage" if the response can be parsed as JSON.
JSON.parse(exception.response.to_str)['resultMessage']
rescue
# Build a generic error message.
"Authlete's #{path} API failed."
end
end
def emit_rack_error_message(request, message)
begin
# Logging if possible.
request.env['rack.errors'].write("ERROR: #{message}\n")
rescue => e
end
end
def to_query(params)
if params.nil? || params.size == 0
return ""
end
array = []
params.each do |key, value|
array.push("#{key}=#{value}")
end
return "?" + array.join("&")
end
public
# Call Authlete's /api/service/create API.
#
# service is the content of a new service to create. The type of
# the given object is either Hash or any object which
# responds to to_hash. In normal cases, Authlete::Model::Service
# (which responds to to_hash) should be used.
#
# On success, an instance of Authlete::Model::ServiceList is returned.
# On error, Authlete::Exception is raised.
def service_create(service)
if service.kind_of?(Hash) == false
if service.respond_to?('to_hash')
service = service.to_hash
end
end
hash = call_api_json_service_owner("/api/service/create", service)
Authlete::Model::Service.new(hash)
end
# Call Authlete's /api/service/delete/{api_key} API.
#
# On error, Authlete::Exception is raised.
def service_delete(api_key)
call_api_service_owner(:delete, "/api/service/delete/#{api_key}", nil, nil)
end
# Call Authlete's /api/service/get/{api_key} API.
#
# api_key is the API key of the service whose information
# you want to get.
#
# On success, an instance of Authlete::Model::Service is returned.
# On error, Authlete::Exception is raised.
def service_get(api_key)
hash = call_api_service_owner(:get, "/api/service/get/#{api_key}", nil, nil)
Authlete::Model::Service.new(hash)
end
# Call Authlete's /api/service/get/list API.
#
# params is an optional hash which contains query parameters
# for /api/service/get/list API. :start and :end are
# a start index (inclusive) and an end index (exclusive), respectively.
#
# On success, an instance of Authlete::Model::ServiceList is returned.
# On error, Authlete::Exception is raised.
def service_get_list(params = nil)
hash = call_api_service_owner(:get, "/api/service/get/list#{to_query(params)}", nil, nil)
Authlete::Model::ServiceList.new(hash)
end
# Call Authlete's /api/service/update/{api_key} API.
#
# api_key is the API key of the service whose information
# you want to get.
#
# service is the new content of the service. The type of
# the given object is either Hash or any object which
# responds to to_hash. In normal cases, Authlete::Model::Service
# (which responds to to_hash) should be used.
#
# On success, an instance of Authlete::Model::Service is returned.
# On error, Authlete::Exception is raised.
def service_update(api_key, service)
if service.kind_of?(Hash) == false
if service.respond_to?('to_hash')
service = service.to_hash
end
end
hash = call_api_json_service_owner("/api/service/update/#{api_key}", service)
Authlete::Model::Service.new(hash)
end
# Call Authlete's /api/serviceowner/get/self API.
#
# On success, an instance of Authlete::Model::ServiceOwner is returned.
# On error, Authlete::Exception is raised.
def serviceowner_get_self
hash = call_api_service_owner(:get, "/api/serviceowner/get/self", nil, nil)
Authlete::Model::ServiceOwner.new(hash)
end
# Call Authlete's /api/client/create API.
#
# client is the content of a new service to create. The type of
# the given object is either Hash or any object which
# responds to to_hash. In normal cases, Authlete::Model::Client
# (which responds to to_hash) should be used.
#
# On success, an instance of Authlete::Model::ClientList is returned.
# On error, Authlete::Exception is raised.
def client_create(client)
if client.kind_of?(Hash) == false
if client.respond_to?('to_hash')
client = client.to_hash
end
end
hash = call_api_json_service("/api/client/create", client)
Authlete::Model::Client.new(hash)
end
# Call Authlete's /api/client/delete/{clientId} API.
#
# On error, Authlete::Exception is raised.
def client_delete(clientId)
call_api_service(:delete, "/api/client/delete/#{clientId}", nil, nil)
end
# Call Authlete's /api/client/get/{clientId} API.
#
# On success, an instance of Authlete::Model::Service is returned.
# On error, Authlete::Exception is raised.
def client_get(clientId)
hash = call_api_service(:get, "/api/client/get/#{clientId}", nil, nil)
Authlete::Model::Client.new(hash)
end
# Call Authlete's /api/client/get/list API.
#
# params is an optional hash which contains query parameters
# for /api/client/get/list API. :start and :end are
# a start index (inclusive) and an end index (exclusive), respectively.
#
# On success, an instance of Authlete::Model::ClientList is returned.
# On error, Authlete::Exception is raised.
def client_get_list(params = nil)
hash = call_api_service(:get, "/api/client/get/list#{to_query(params)}", nil, nil)
Authlete::Model::ClientList.new(hash)
end
# Call Authlete's /api/client/update/{clientId} API.
#
# client is the new content of the client. The type of
# the given object is either Hash or any object which
# responds to to_hash. In normal cases, Authlete::Model::Client
# (which responds to to_hash) should be used.
#
# On success, an instance of Authlete::Model::Client is returned.
# On error, Authlete::Exception is raised.
def client_update(client)
if client.kind_of?(Hash) == false
if client.respond_to?('to_hash')
client = client.to_hash
end
end
hash = call_api_json_service("/api/client/update/#{client[:clientId]}", client)
Authlete::Model::Client.new(hash)
end
# Call Authlete's {/auth/introspection}
# [https://www.authlete.com/authlete_web_apis_introspection.html#auth_introspection]
# API.
#
# token is an access token presented by a client application.
# This is a must parameter. In a typical case, a client application uses
# one of the means listed in {RFC 6750}[https://tools.ietf.org/html/rfc6750]
# to present an access token to a {protected resource endpoint}
# [https://tools.ietf.org/html/rfc6749#section-7].
#
# scopes is an array of scope names. This is an optional parameter.
# When the specified scopes are not covered by the access token, Authlete
# prepares the content of the error response.
#
# subject is a unique identifier of an end-user. This is an optional
# parameter. When the access token is not associated with the specified
# subject, Authlete prepares the content of the error response.
#
# On success, this method returns an instance of
# Authlete::Response::IntrospectionResponse. On error, this method
# throws RestClient::Exception.
def introspection(token, scopes = nil, subject = nil)
hash = call_api_json_service('/api/auth/introspection',
:token => token, :scopes => scopes, :subject => subject)
Authlete::Response::IntrospectionResponse.new(hash)
end
# Ensure that the request contains a valid access token.
#
# This method extracts an access token from the given request based on the
# rules described in RFC 6750 and introspects the access token by calling
# Authlete's /auth/introspection API.
#
# The first argument request is a Rack request.
#
# The second argument scopes is an array of scope names required
# to access the target protected resource. This argument is optional.
#
# The third argument subject is a string which representing a
# subject which has to be associated with the access token. This argument
# is optional.
#
# This method returns an instance of
# Authlete::Response::IntrospectionResponse. If its action
# method returns 'OK', it means that the access token exists, has not
# expired, covers the requested scopes (if specified), and is associated
# with the requested subject (if specified). Otherwise, it means that the
# request does not contain any access token or that the access token does
# not satisfy the conditions to access the target protected resource.
def protect_resource(request, scopes = nil, subject = nil)
# Extract an access token from the request.
access_token = extract_access_token(request)
# If the request does not contain any access token.
if access_token.nil?
# The request does not contain a valid access token.
return Authlete::Response::IntrospectionResponse.new(
:action => 'BAD_REQUEST',
:responseContent => 'Bearer error="invalid_token",error_description="The request does not contain a valid access token."'
)
end
begin
# Call Authlete's /auth/introspection API to introspect the access token.
result = introspection(access_token, scopes, subject)
rescue => e
# Error message.
message = build_error_message('/auth/introspection', e)
# Emit a Rack error message.
emit_rack_error_message(request, message)
# Failed to introspect the access token.
return Authlete::Response::IntrospectionResponse.new(
:action => 'INTERNAL_SERVER_ERROR',
:responseContent => "Bearer error=\"server_error\",error_description=\"#{message}\""
)
end
# Return the response from Authlete's /auth/introspection API.
result
end
end
end