├── auth ├── __init__.py ├── endpoints.py ├── handlers.py └── models.py ├── .gitmodules ├── ext └── __init__.py ├── app.yaml ├── main.py ├── .gitignore ├── helloworld_api.py ├── README.md └── LICENSE /auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/simpleauth"] 2 | path = ext/simpleauth 3 | url = https://github.com/crhym3/simpleauth.git 4 | [submodule "ext/python-oauth2"] 5 | path = ext/python-oauth2 6 | url = https://github.com/simplegeo/python-oauth2.git 7 | [submodule "ext/httplib2"] 8 | path = ext/httplib2 9 | url = https://github.com/jcgregorio/httplib2.git 10 | -------------------------------------------------------------------------------- /auth/endpoints.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .models import User 4 | 5 | 6 | def get_current_user(): 7 | token = os.getenv('HTTP_AUTHORIZATION') 8 | if token: 9 | try: 10 | token = token.split(' ')[1] 11 | except IndexError: 12 | pass 13 | 14 | user, _ = User.get_by_bearer_token(token) 15 | return user 16 | -------------------------------------------------------------------------------- /ext/__init__.py: -------------------------------------------------------------------------------- 1 | """This module imports third-party packages which have been linked via ``git submodule`` 2 | 3 | This is necessary since most root repository directories aren't python modules themselves. 4 | """ 5 | 6 | import os 7 | import sys 8 | 9 | _modules = [ 10 | ('httplib2','python2',), 11 | ('python-oauth2',), 12 | ('simpleauth',) 13 | ] 14 | 15 | for module in _modules: 16 | sys.path.append(os.path.join(os.path.dirname(__file__), *module)) 17 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: your-app-id 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: yes 6 | 7 | builtins: 8 | - remote_api: on 9 | 10 | handlers: 11 | # Endpoints handler 12 | - url: /_ah/spi/.* 13 | script: helloworld_api.APPLICATION 14 | # webapp2 handler 15 | - url: /.* 16 | script: main.app 17 | secure: always 18 | 19 | libraries: 20 | - name: pycrypto 21 | version: latest 22 | - name: endpoints 23 | version: 1.0 24 | - name: webapp2 25 | version: latest 26 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from webapp2 import WSGIApplication, Route 3 | 4 | # Make sure external paths are correctly set 5 | import ext 6 | 7 | # webapp2 config 8 | config = { 9 | 'webapp2_extras.auth': { 10 | 'user_model': 'auth.models.User' 11 | } 12 | } 13 | 14 | debug = os.environ.get('SERVER_SOFTWARE', '').startswith('Dev') 15 | 16 | routes = [ 17 | Route('/oauth2/access_token', handler='auth.handlers.AuthHandler', name='auth_access_token'), 18 | ] 19 | 20 | app = WSGIApplication(routes, config=config, debug=debug) 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /helloworld_api.py: -------------------------------------------------------------------------------- 1 | import endpoints 2 | from protorpc import messages 3 | from protorpc import message_types 4 | from protorpc import remote 5 | 6 | 7 | from auth.endpoints import get_current_user 8 | 9 | package = 'Hello' 10 | 11 | 12 | class Greeting(messages.Message): 13 | """Greeting that stores a message.""" 14 | message = messages.StringField(1) 15 | 16 | 17 | class GreetingCollection(messages.Message): 18 | """Collection of Greetings.""" 19 | items = messages.MessageField(Greeting, 1, repeated=True) 20 | 21 | 22 | STORED_GREETINGS = GreetingCollection(items=[ 23 | Greeting(message='hello world!'), 24 | Greeting(message='goodbye world!'), 25 | ]) 26 | 27 | 28 | @endpoints.api(name='helloworld', version='v1') 29 | class HelloWorldApi(remote.Service): 30 | """Helloworld API v1.""" 31 | 32 | @endpoints.method(message_types.VoidMessage, GreetingCollection, 33 | path='hellogreeting', http_method='GET', 34 | name='greetings.listGreeting') 35 | def greetings_list(self, *args): 36 | user = get_current_user() 37 | if not user: 38 | raise endpoints.UnauthorizedException() 39 | 40 | return STORED_GREETINGS 41 | 42 | 43 | APPLICATION = endpoints.api_server([HelloWorldApi]) 44 | -------------------------------------------------------------------------------- /auth/handlers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import webapp2 4 | from webapp2_extras import auth 5 | 6 | from simpleauth import SimpleAuthHandler 7 | 8 | 9 | class AuthHandler(webapp2.RequestHandler, SimpleAuthHandler): 10 | """Authenticates a user to the application via a third-party provider. 11 | 12 | The return value of this request is an OAuth token response. 13 | 14 | Only a subset of the PROVIDERS specified in SimpleAuthHandler are currently supported. 15 | Tested providers: Facebook 16 | """ 17 | def _on_signin(self, data, auth_info, provider): 18 | # Create the auth ID format used by the User model 19 | auth_id = '%s:%s' % (provider, data['id']) 20 | user_model = auth.get_auth().store.user_model 21 | user = user_model.get_by_auth_id(auth_id) 22 | 23 | if not user: 24 | ok, user = user_model.create_user(auth_id) 25 | if not ok: 26 | logging.error('Unable to create user for auth_id %s' % auth_id) 27 | self.abort(500, 'Unable to create user') 28 | 29 | return user 30 | 31 | def post(self): 32 | # TODO: Consider adding a check for a valid client ID here as well. 33 | 34 | access_token = self.request.get('x_access_token') 35 | provider = self.request.get('x_provider') 36 | 37 | if provider not in self.PROVIDERS or access_token is None: 38 | self.abort(401, 'Unknown provider or access token') 39 | 40 | auth_info = {'access_token': access_token} 41 | fetch_user_info = getattr(self, '_get_%s_user_info' % provider) 42 | user_info = fetch_user_info(auth_info) 43 | 44 | if 'id' in user_info: 45 | user = self._on_signin(user_info, auth_info, provider) 46 | token = user.create_bearer_token(user.get_id()) 47 | 48 | self.response.content_type = 'application/json' 49 | self.response.body = json.dumps({ 50 | 'access_token': token.token, 51 | 'token_type': 'Bearer', 52 | 'expires_in': token.bearer_token_timedelta.total_seconds(), 53 | 'refresh_token': token.refresh_token 54 | }) 55 | else: 56 | self.abort(401, 'Access token is invalid') 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | An example Google AppEngine project using Cloud Endpoints and custom authentication. 2 | 3 | The supported workflow: 4 | - A mobile client authenticates via a third-party provider using a native SDK flow, in this case with Facebook. 5 | - The Facebook access token is sent to the AppEngine app, who verifies and returns its own access token in response, creating a new User entity if necessary. 6 | - The client includes that token in each endpoints service request using in the `Authorization` header. 7 | - The endpoints method uses the access token to retrieve the authenticated user. 8 | 9 | This is intentionally a narrow use case, but should help inspire ideas on different approaches as well. 10 | 11 | ## Dependencies 12 | 1. [webapp2](http://webapp-improved.appspot.com/index.html) is used for the access token exchange handler 13 | 2. [webapp2_extras.appengine](http://webapp-improved.appspot.com/api/webapp2_extras/appengine/auth/models.html) provides a custom User model 14 | 3. [simpleauth](https://github.com/crhym3/simpleauth) is included a submodule and is used to verify provider access tokens 15 | 16 | ## Usage 17 | 18 | 1. Check out the project and submodules 19 | ```bash 20 | git clone git@github.com:loudnate/appengine-endpoints-auth-example.git 21 | git submodule update 22 | ``` 23 | 24 | 2. [Generate a client application](https://developers.google.com/appengine/docs/python/endpoints/gen_clients) for your endpoints 25 | 3. Include the [Facebook SDK](https://developers.facebook.com/docs/facebook-login) and implement a login flow 26 | 4. Exchange the Facebook access token for one provided by your app 27 | ```bash 28 | POST /oauth2/access_token HTTP/1.1 29 | Host: https://your-app-id.appspot.com 30 | Cache-Control: no-cache 31 | Content-Type: application/x-www-form-urlencoded 32 | 33 | x_access_token=facebook-access-token&x_provider=facebook 34 | ``` 35 | ```json 36 | { 37 | "token_type": "Bearer", 38 | "refresh_token": "6oqmYZSaQ72nZfEYlD5PZF", 39 | "access_token": "nc7Omfm4vgP0swqodJyDeN", 40 | "expires_in": 31536000 41 | } 42 | ``` 43 | ```obj-c 44 | AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; 45 | NSDictionary *parameters = @{@"x_access_token": @"facebook-access-token", @"x_provider": @"facebook"}; 46 | 47 | manager.responseSerializer = [AFJSONResponseSerializer serializer]; 48 | [manager POST:@"https://your-app-id.appspot.com/oauth2/access_token" 49 | parameters:parameters 50 | success:^(AFHTTPRequestOperation *operation, id responseObject) { 51 | // ... 52 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 53 | // ... 54 | }]; 55 | ``` 56 | 5. Store the credentials [somewhere appropriate](https://developer.apple.com/library/ios/documentation/security/conceptual/keychainServConcepts/02concepts/concepts.html) and send them with each endpoints service request 57 | ```obj-c 58 | GTLServiceHelloworld *service = [[GTLServiceHelloworld alloc] init]; 59 | NSString *authHeaderValue = [NSString stringWithFormat:@"%@ %@", responseObject[@"token_type"], responseObject[@"access_token"]]; 60 | 61 | service.additionalHTTPHeaders = @{@"Authorization": authHeaderValue}; 62 | ``` 63 | -------------------------------------------------------------------------------- /auth/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from datetime import timedelta 3 | import logging 4 | 5 | from google.appengine.ext.ndb import model 6 | from webapp2_extras import security 7 | from webapp2_extras.appengine.auth.models import Unique 8 | from webapp2_extras.appengine.auth.models import User as BaseUserExpando 9 | from webapp2_extras.appengine.auth.models import UserToken as BaseUserToken 10 | 11 | 12 | class UserToken(BaseUserToken): 13 | SUBJECT_BEARER = 'bearer' 14 | 15 | unique_model = Unique 16 | bearer_token_timedelta = timedelta(days=365) 17 | 18 | refresh_token = model.StringProperty() 19 | 20 | @classmethod 21 | def create(cls, user, subject, token=None): 22 | if subject == cls.SUBJECT_BEARER: 23 | user = str(user) 24 | token = token or security.generate_random_string(entropy=128) 25 | 26 | # Bearer tokens must be unique on their own, without a user scope. 27 | key = cls.get_key('', subject, token) 28 | entity = cls( 29 | key=key, 30 | user=user, 31 | subject=subject, 32 | token=token, 33 | refresh_token=security.generate_random_string(entropy=128) 34 | ) 35 | 36 | # Refresh tokens must be unique 37 | ok = cls.unique_model.create( 38 | '%s.refresh_token:%s' % (cls.__name__, entity.refresh_token) 39 | ) 40 | if ok: 41 | entity.put() 42 | else: 43 | logging.warning('Unable to create a unique user token for user %s', user) 44 | entity = None 45 | else: 46 | entity = super(UserToken, cls).create(user, subject, token) 47 | 48 | return entity 49 | 50 | def expires_at(self): 51 | """Returns the datetime after which this token is no longer valid 52 | 53 | :returns: 54 | A datetime object after which this token is no longer valid. 55 | """ 56 | if self.subject == self.SUBJECT_BEARER: 57 | return self.created + self.bearer_token_timedelta 58 | 59 | return None 60 | 61 | def is_expired(self): 62 | """Whether the token is past its expiry time 63 | 64 | :returns: 65 | True if the token has expired 66 | """ 67 | return self.expires_at() <= datetime.now() 68 | 69 | 70 | class User(BaseUserExpando): 71 | token_model = UserToken 72 | 73 | @classmethod 74 | def get_by_bearer_token(cls, token): 75 | """Returns a user object based on a user ID and oauth bearer token. 76 | 77 | :param token: 78 | The token string to be verified. 79 | :returns: 80 | A tuple ``(User, timestamp)``, with a user object and 81 | the token timestamp, or ``(None, None)`` if both were not found. 82 | """ 83 | if token: 84 | token_obj = cls.token_model.get('', 'bearer', token) 85 | if token_obj and not token_obj.is_expired(): 86 | user = cls.get_by_id(int(token_obj.user)) 87 | if user: 88 | return user, token_obj.created 89 | 90 | return None, None 91 | 92 | @classmethod 93 | def create_bearer_token(cls, user_id): 94 | """Creates a new oauth bearer token for a given user ID. 95 | 96 | :param user_id: 97 | User unique ID. 98 | :returns: 99 | A token object, or None if one could not be created. 100 | """ 101 | return cls.token_model.create(user_id, 'bearer') 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nate Racklyeft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | ----- 25 | 26 | Portions of this software are sourced from "Hello Endpoints Python", licensed under Apache v2 27 | https://github.com/GoogleCloudPlatform/appengine-endpoints-helloendpoints-python 28 | 29 | 30 | 31 | Apache License 32 | Version 2.0, January 2004 33 | http://www.apache.org/licenses/ 34 | 35 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 36 | 37 | 1. Definitions. 38 | 39 | "License" shall mean the terms and conditions for use, reproduction, 40 | and distribution as defined by Sections 1 through 9 of this document. 41 | 42 | "Licensor" shall mean the copyright owner or entity authorized by 43 | the copyright owner that is granting the License. 44 | 45 | "Legal Entity" shall mean the union of the acting entity and all 46 | other entities that control, are controlled by, or are under common 47 | control with that entity. For the purposes of this definition, 48 | "control" means (i) the power, direct or indirect, to cause the 49 | direction or management of such entity, whether by contract or 50 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 51 | outstanding shares, or (iii) beneficial ownership of such entity. 52 | 53 | "You" (or "Your") shall mean an individual or Legal Entity 54 | exercising permissions granted by this License. 55 | 56 | "Source" form shall mean the preferred form for making modifications, 57 | including but not limited to software source code, documentation 58 | source, and configuration files. 59 | 60 | "Object" form shall mean any form resulting from mechanical 61 | transformation or translation of a Source form, including but 62 | not limited to compiled object code, generated documentation, 63 | and conversions to other media types. 64 | 65 | "Work" shall mean the work of authorship, whether in Source or 66 | Object form, made available under the License, as indicated by a 67 | copyright notice that is included in or attached to the work 68 | (an example is provided in the Appendix below). 69 | 70 | "Derivative Works" shall mean any work, whether in Source or Object 71 | form, that is based on (or derived from) the Work and for which the 72 | editorial revisions, annotations, elaborations, or other modifications 73 | represent, as a whole, an original work of authorship. For the purposes 74 | of this License, Derivative Works shall not include works that remain 75 | separable from, or merely link (or bind by name) to the interfaces of, 76 | the Work and Derivative Works thereof. 77 | 78 | "Contribution" shall mean any work of authorship, including 79 | the original version of the Work and any modifications or additions 80 | to that Work or Derivative Works thereof, that is intentionally 81 | submitted to Licensor for inclusion in the Work by the copyright owner 82 | or by an individual or Legal Entity authorized to submit on behalf of 83 | the copyright owner. For the purposes of this definition, "submitted" 84 | means any form of electronic, verbal, or written communication sent 85 | to the Licensor or its representatives, including but not limited to 86 | communication on electronic mailing lists, source code control systems, 87 | and issue tracking systems that are managed by, or on behalf of, the 88 | Licensor for the purpose of discussing and improving the Work, but 89 | excluding communication that is conspicuously marked or otherwise 90 | designated in writing by the copyright owner as "Not a Contribution." 91 | 92 | "Contributor" shall mean Licensor and any individual or Legal Entity 93 | on behalf of whom a Contribution has been received by Licensor and 94 | subsequently incorporated within the Work. 95 | 96 | 2. Grant of Copyright License. Subject to the terms and conditions of 97 | this License, each Contributor hereby grants to You a perpetual, 98 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 99 | copyright license to reproduce, prepare Derivative Works of, 100 | publicly display, publicly perform, sublicense, and distribute the 101 | Work and such Derivative Works in Source or Object form. 102 | 103 | 3. Grant of Patent License. Subject to the terms and conditions of 104 | this License, each Contributor hereby grants to You a perpetual, 105 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 106 | (except as stated in this section) patent license to make, have made, 107 | use, offer to sell, sell, import, and otherwise transfer the Work, 108 | where such license applies only to those patent claims licensable 109 | by such Contributor that are necessarily infringed by their 110 | Contribution(s) alone or by combination of their Contribution(s) 111 | with the Work to which such Contribution(s) was submitted. If You 112 | institute patent litigation against any entity (including a 113 | cross-claim or counterclaim in a lawsuit) alleging that the Work 114 | or a Contribution incorporated within the Work constitutes direct 115 | or contributory patent infringement, then any patent licenses 116 | granted to You under this License for that Work shall terminate 117 | as of the date such litigation is filed. 118 | 119 | 4. Redistribution. You may reproduce and distribute copies of the 120 | Work or Derivative Works thereof in any medium, with or without 121 | modifications, and in Source or Object form, provided that You 122 | meet the following conditions: 123 | 124 | (a) You must give any other recipients of the Work or 125 | Derivative Works a copy of this License; and 126 | 127 | (b) You must cause any modified files to carry prominent notices 128 | stating that You changed the files; and 129 | 130 | (c) You must retain, in the Source form of any Derivative Works 131 | that You distribute, all copyright, patent, trademark, and 132 | attribution notices from the Source form of the Work, 133 | excluding those notices that do not pertain to any part of 134 | the Derivative Works; and 135 | 136 | (d) If the Work includes a "NOTICE" text file as part of its 137 | distribution, then any Derivative Works that You distribute must 138 | include a readable copy of the attribution notices contained 139 | within such NOTICE file, excluding those notices that do not 140 | pertain to any part of the Derivative Works, in at least one 141 | of the following places: within a NOTICE text file distributed 142 | as part of the Derivative Works; within the Source form or 143 | documentation, if provided along with the Derivative Works; or, 144 | within a display generated by the Derivative Works, if and 145 | wherever such third-party notices normally appear. The contents 146 | of the NOTICE file are for informational purposes only and 147 | do not modify the License. You may add Your own attribution 148 | notices within Derivative Works that You distribute, alongside 149 | or as an addendum to the NOTICE text from the Work, provided 150 | that such additional attribution notices cannot be construed 151 | as modifying the License. 152 | 153 | You may add Your own copyright statement to Your modifications and 154 | may provide additional or different license terms and conditions 155 | for use, reproduction, or distribution of Your modifications, or 156 | for any such Derivative Works as a whole, provided Your use, 157 | reproduction, and distribution of the Work otherwise complies with 158 | the conditions stated in this License. 159 | 160 | 5. Submission of Contributions. Unless You explicitly state otherwise, 161 | any Contribution intentionally submitted for inclusion in the Work 162 | by You to the Licensor shall be under the terms and conditions of 163 | this License, without any additional terms or conditions. 164 | Notwithstanding the above, nothing herein shall supersede or modify 165 | the terms of any separate license agreement you may have executed 166 | with Licensor regarding such Contributions. 167 | 168 | 6. Trademarks. This License does not grant permission to use the trade 169 | names, trademarks, service marks, or product names of the Licensor, 170 | except as required for reasonable and customary use in describing the 171 | origin of the Work and reproducing the content of the NOTICE file. 172 | 173 | 7. Disclaimer of Warranty. Unless required by applicable law or 174 | agreed to in writing, Licensor provides the Work (and each 175 | Contributor provides its Contributions) on an "AS IS" BASIS, 176 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 177 | implied, including, without limitation, any warranties or conditions 178 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 179 | PARTICULAR PURPOSE. You are solely responsible for determining the 180 | appropriateness of using or redistributing the Work and assume any 181 | risks associated with Your exercise of permissions under this License. 182 | 183 | 8. Limitation of Liability. In no event and under no legal theory, 184 | whether in tort (including negligence), contract, or otherwise, 185 | unless required by applicable law (such as deliberate and grossly 186 | negligent acts) or agreed to in writing, shall any Contributor be 187 | liable to You for damages, including any direct, indirect, special, 188 | incidental, or consequential damages of any character arising as a 189 | result of this License or out of the use or inability to use the 190 | Work (including but not limited to damages for loss of goodwill, 191 | work stoppage, computer failure or malfunction, or any and all 192 | other commercial damages or losses), even if such Contributor 193 | has been advised of the possibility of such damages. 194 | 195 | 9. Accepting Warranty or Additional Liability. While redistributing 196 | the Work or Derivative Works thereof, You may choose to offer, 197 | and charge a fee for, acceptance of support, warranty, indemnity, 198 | or other liability obligations and/or rights consistent with this 199 | License. However, in accepting such obligations, You may act only 200 | on Your own behalf and on Your sole responsibility, not on behalf 201 | of any other Contributor, and only if You agree to indemnify, 202 | defend, and hold each Contributor harmless for any liability 203 | incurred by, or claims asserted against, such Contributor by reason 204 | of your accepting any such warranty or additional liability. 205 | 206 | END OF TERMS AND CONDITIONS 207 | 208 | APPENDIX: How to apply the Apache License to your work. 209 | 210 | To apply the Apache License to your work, attach the following 211 | boilerplate notice, with the fields enclosed by brackets "[]" 212 | replaced with your own identifying information. (Don't include 213 | the brackets!) The text should be enclosed in the appropriate 214 | comment syntax for the file format. We also recommend that a 215 | file or class name and description of purpose be included on the 216 | same "printed page" as the copyright notice for easier 217 | identification within third-party archives. 218 | 219 | Copyright [yyyy] [name of copyright owner] 220 | 221 | Licensed under the Apache License, Version 2.0 (the "License"); 222 | you may not use this file except in compliance with the License. 223 | You may obtain a copy of the License at 224 | 225 | http://www.apache.org/licenses/LICENSE-2.0 226 | 227 | Unless required by applicable law or agreed to in writing, software 228 | distributed under the License is distributed on an "AS IS" BASIS, 229 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 230 | See the License for the specific language governing permissions and 231 | limitations under the License. --------------------------------------------------------------------------------