├── OAuth2_0-SampleApp.ini ├── OAuth2_0-SampleApp.py ├── OAuthJDBCTest.java ├── PKCE.py ├── README.md ├── attributes.html ├── bad.html ├── logonform.html ├── main.html └── token.html /OAuth2_0-SampleApp.ini: -------------------------------------------------------------------------------- 1 | # before operating these settings you should likely read 2 | # the Snowflake's OAuth docs here: 3 | # for Snowflake OAuth (where Snowflake acts as the authorization service): 4 | # https://docs.snowflake.net/manuals/user-guide/oauth-custom.html 5 | # for External OAuth (where a 3rd party acts as the authorization service): 6 | # https://docs.snowflake.com/en/user-guide/oauth-ext-custom.html 7 | 8 | [APP] 9 | # what port will the ap listen on 10 | port = 8088 11 | 12 | [OAUTH] 13 | # this section is for the Snowflake OAuth settings. 14 | # this will be for the Snowflake OAuth setting and 15 | # the section below will be for External OAuth 16 | 17 | # these are the most sensitive values there are here. 18 | # in a prodction setting, strongly consider managing 19 | # them with a seecrets management platform. Read about 20 | # getting these secrets with the Snowflake function here: 21 | # https://docs.snowflake.net/manuals/sql-reference/functions/system_show_oauth_client_secrets.html 22 | client_id = YOURSNOWFLAKEOAUTHCLIENTID 23 | client_secret = YOURSNOWFLAKEOAUTHCLIENTSECRET 24 | 25 | # this will be set to the same value used when configuring 26 | # the interation in Snowflake, and the URL that will be 27 | # used to reach the app while it's running. 28 | redirect_uri = http://127.0.0.1:${APP:port}/ 29 | 30 | # you should only need to set the 'snwoflake_uri' here. 31 | # the others should be correct as is. 32 | snowflake_uri = https://...snowflakecomputing.com 33 | authorization_endpoint = ${snowflake_uri}/oauth/authorize 34 | token_endpoint = ${snowflake_uri}/oauth/token-request 35 | 36 | # if your intergation was configured to do PKCE 37 | # enforcement, then set this to TRUE or leave it 38 | # blank. 39 | do_pkce = TRUE 40 | 41 | [EXTOAUTH] 42 | # this is the scope you will use for the session, which 43 | # will map to the role(s) which will be available. this 44 | # could be something like a URL or a string depending on 45 | # the 3rd party involved 46 | extoauth_scope = YOUREXTERNALOAUTHSCOPE 47 | 48 | # these are the most sensitive values there are here. 49 | # in a prodction setting, strongly consider managing 50 | # them with a seecrets management platform. 51 | extoauth_oauth_client_id = YOUREXTERNALOAUTHCLIENTID 52 | extoauth_oauth_client_secret = YOUREXTERNALOAUTHCLIENTSECRET 53 | 54 | # these will be the URLs of the third party's endpoints 55 | # and issuers 56 | extoauth_token_endpoint = https://your.idp.token.endpoint/path/token_endpoint 57 | extoauth_jws_key_endpoint = https://your.idp.jws_key.endpoint/path/keys 58 | extoauth_issuer = https://your.idp.issuer/ # there are some situations where this may be a string not a url 59 | 60 | [JAVA] 61 | # to run the JDBC case there needs to be both a 62 | # classpath pointing to where the compiled OAuthJDBCTest 63 | # is and another to the Snowflake JDBC jar file 64 | compiled_classpath = /full/path/to/javac/compiled/class/directory # do not put class file name here 65 | snowflake_jdbc_classpath = /full/path/to/JDBC/jar/file/snowflake-jdbc-3.12.9.jar # update jar's name with your version 66 | 67 | [SNOWFLAKE] 68 | # this section is for the Snowflake connection 69 | # settings for the python connector. 70 | 71 | # you want the user NAME here, not LOGIN_NAME. 72 | ### NOT CURRENTLY USED #### user = WADEWILSON 73 | 74 | # the 'account' is used for the formation of a connection string 75 | # and should be the full .. form. the 76 | # 'accountname' is used for JDBC account matching and should 77 | # only be the portion of the URI 78 | account = .. 79 | accounturl = ...snowflakecomputing.com 80 | ### NOT CURRENTLY USED #### accountname = va_demo06 81 | 82 | # this should not be changed. 83 | authenticator = OAUTH 84 | 85 | # this is the role which will be used to request External 86 | # OAuth tokens and should br a role granted to the user 87 | # you use to authenticate 88 | role = YOURSNOWFLAKEROLE 89 | 90 | # this query should return NO MORE THAN 2 columns in 91 | # its results. for readability in the UI, it's suggested 92 | # to return no more than 5-10 rows 93 | query = select PRIME, SEQUENTIALRANK from MATH.PUBLIC.PRIMES limit 5 # this SQL will not work out of the box 94 | -------------------------------------------------------------------------------- /OAuth2_0-SampleApp.py: -------------------------------------------------------------------------------- 1 | ################################################################### 2 | ################################################################### 3 | ## The MIT License - SPDX short identifier: MIT ## 4 | ################################################################### 5 | ################################################################### 6 | # 7 | #Copyright 2019 @sanderiam & https://github.com/snowflakedb 8 | # 9 | #Permission is hereby granted, free of charge, to any person obtaining 10 | #a copy of this software and associated documentation files (the "Software"), 11 | #to deal in the Software without restriction, including without 12 | #limitation the rights to use, copy, modify, merge, publish, distribute, 13 | #sublicense, and/or sell copies of the Software, and to permit persons 14 | #to whom the Software is furnished to do so, subject to the following 15 | #conditions: 16 | # 17 | #The above copyright notice and this permission notice shall be 18 | #included in all copies or substantial portions of the Software. 19 | # 20 | #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | #EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | #MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 23 | #IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | #CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 25 | #TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 26 | #SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | 29 | # Please consider this script an example. 30 | # Do not use this in any production scenario 31 | 32 | from bottle import Bottle, request, run, redirect, SimpleTemplate, template 33 | from urllib.parse import urlparse, urlunparse, urlencode, quote 34 | import urllib.request 35 | import requests 36 | import string 37 | import random 38 | import base64 39 | import ssl 40 | import json 41 | import subprocess 42 | from subprocess import STDOUT,PIPE 43 | import snowflake.connector 44 | import configparser 45 | from PKCE import code_verifier, code_challenge 46 | 47 | # define the bottle app that will wrap around all this 48 | app = Bottle() 49 | 50 | # begin an instance of the INI reader 51 | config = configparser.ConfigParser() 52 | config._interpolation = configparser.ExtendedInterpolation() 53 | 54 | # import the coniguration from the ini file in this directory 55 | # change to a full path if you move the file 56 | config.read('OAuth2_0-SampleApp.ini') 57 | 58 | # App settings 59 | app_port = config['APP']['port'] 60 | 61 | # Snowflake OAuth settings 62 | client_id = config['OAUTH']['client_id'] 63 | client_secret = config['OAUTH']['client_secret'] 64 | redirect_uri = config['OAUTH']['redirect_uri'] 65 | authorization_endpoint = config['OAUTH']['authorization_endpoint'] 66 | token_endpoint = config['OAUTH']['token_endpoint'] 67 | do_pkce = config['OAUTH']['do_pkce'] 68 | 69 | # External OAuth settings 70 | extoauth_scope = config['EXTOAUTH']['extoauth_scope'] 71 | extoauth_oauth_client_id = config['EXTOAUTH']['extoauth_oauth_client_id'] 72 | extoauth_oauth_client_secret = config['EXTOAUTH']['extoauth_oauth_client_secret'] 73 | extoauth_token_endpoint = config['EXTOAUTH']['extoauth_token_endpoint'] 74 | extoauth_jws_key_endpoint = config['EXTOAUTH']['extoauth_jws_key_endpoint'] 75 | extoauth_issuer = config['EXTOAUTH']['extoauth_issuer'] 76 | 77 | # Java settings 78 | java_class = config['JAVA']['compiled_classpath'] 79 | java_jdbcjar = config['JAVA']['snowflake_jdbc_classpath'] 80 | 81 | # Snowflake settings 82 | snowflake_account = config['SNOWFLAKE']['account'] 83 | snowflake_accounturl = config['SNOWFLAKE']['accounturl'] 84 | snowflake_authenticator = config['SNOWFLAKE']['authenticator'] 85 | snowflake_role = config['SNOWFLAKE']['role'] 86 | snowflake_query = config['SNOWFLAKE']['query'] 87 | 88 | # if PKCE has been set to TRUE, generate the code verifier and challenge 89 | # see https://tools.ietf.org/html/rfc7636 for details 90 | if do_pkce == "TRUE": 91 | code_verifier = code_verifier() 92 | code_challenge = code_challenge(code_verifier) 93 | 94 | # generate a string to use for the OAuth state to protect against CSRF 95 | # see https://tools.ietf.org/id/draft-bradley-oauth-jwt-encoded-state-08.html 96 | # for details 97 | def string_num_generator(size): 98 | chars = string.ascii_uppercase + string.ascii_lowercase + string.digits 99 | return ''.join(random.choice(chars) for _ in range(size)) 100 | 101 | state_parameter = string_num_generator(150) 102 | 103 | # this dict will maintain the state of the token throughout the run of the program 104 | state = {} 105 | 106 | # the index page for the application outputs a login prompt 107 | @app.get('/') 108 | def do_get(): 109 | # UNCOMMENT FOR DEBUGGING # print('ENTERING: do_get') 110 | code = request.query.get('code') 111 | if code: 112 | returned_state_parameter = request.query.get('state') 113 | # UNCOMMENT FOR DEBUGGING # print('DEBUGGING... setting returned_state_parameter to a bad value on purpose') 114 | # UNCOMMENT FOR DEBUGGING # returned_state_parameter = 'noway' # use to test inproper returns 115 | 116 | if returned_state_parameter != state_parameter: 117 | return template('bad.html', returned_state_parameter=returned_state_parameter, state_parameter=state_parameter) 118 | 119 | # got code from OAuth 2 authentication server 120 | token = get_token_code(code) 121 | state.update(token) 122 | return template('token.html', items=token.items(), refresh_token=urllib.parse.quote(token['refresh_token'])) 123 | # UNCOMMENT FOR DEBUGGING # print('state right now: {}'.format(state)) 124 | else: 125 | return template('main.html') 126 | 127 | # handles the forwarding for authentication used for Snowflake OAuth 128 | @app.get('/logon') 129 | def do_logon(): 130 | # UNCOMMENT FOR DEBUGGING # print('ENTERING: do_logon {Snowflake OAuth}') 131 | pr=list(urlparse(authorization_endpoint)) 132 | # set query 133 | if do_pkce == "TRUE": 134 | pr[4]=urlencode({ 135 | 'response_type': 'code', 136 | 'client_id': client_id, 137 | 'redirect_uri': redirect_uri, 138 | 'state': state_parameter, 139 | 'code_challenge': code_challenge, 140 | 'code_challenge_method': 'S256' 141 | }) 142 | else: 143 | pr[4]=urlencode({ 144 | 'response_type': 'code', 145 | 'client_id': client_id, 146 | 'redirect_uri': redirect_uri, 147 | 'state': state_parameter 148 | }) 149 | # perform redirection to OAuth 2 authentication server 150 | # UNCOMMENT FOR DEBUGGING # print('pr for logon: {}'.format(pr)) 151 | redirect(urlunparse(pr)) 152 | 153 | # handles the forwarding for authentication used for Snowflake OAuth 154 | @app.get('/extoauth', method='POST') 155 | def do_extoauth(): 156 | # UNCOMMENT FOR DEBUGGING # print('ENTERING: do_extoauth') 157 | 158 | # Resource owner (enduser) credentials for OAuth 2.0 Resource Owner Password Credentials (ROPC) grant 159 | RO_user = request.forms.get('username') 160 | RO_password = request.forms.get('password') 161 | 162 | data = {'grant_type': 'password', 'username': RO_user, 'password': RO_password, 'scope': extoauth_scope} 163 | 164 | access_token_response = requests.post(extoauth_token_endpoint, data=data, verify=False, allow_redirects=False, auth=(extoauth_oauth_client_id, extoauth_oauth_client_secret)) 165 | 166 | # UNCOMMENT FOR DEBUGGING # print("response headers: ") 167 | # UNCOMMENT FOR DEBUGGING # print(access_token_response.headers) 168 | # UNCOMMENT FOR DEBUGGING # print("response data: " + access_token_response.text) 169 | 170 | tokens = json.loads(access_token_response.text) 171 | # UNCOMMENT FOR DEBUGGING # print("access token: " + tokens['access_token']) 172 | 173 | state.update(tokens) 174 | 175 | refresh = json.loads('{"refresh_token":"null"}') 176 | state.update(refresh) 177 | return template('token.html', items=state.items(), refresh_token='null') 178 | 179 | # used to get the ROPC user's credentials doe External OAuth 180 | @app.get('/logonform') 181 | def do_logonform(): 182 | return template('logonform.html') 183 | 184 | # this will run the supplied SQL in the Python connector 185 | @app.get('/getattr') 186 | def get_attributes(): 187 | # UNCOMMENT FOR DEBUGGING # print('ENTERING: get_attributes') 188 | # UNCOMMENT FOR DEBUGGING # print('state right now: {}'.format(state)) 189 | 190 | cnx = snowflake.connector.connect( 191 | account=snowflake_account, 192 | authenticator=snowflake_authenticator, 193 | role=snowflake_role, 194 | token=state['access_token'] 195 | ) 196 | 197 | # Querying data 198 | cur = cnx.cursor() 199 | 200 | rowdict = {} 201 | 202 | try: 203 | cur.execute(snowflake_query) 204 | 205 | for (col1, col2) in cur: 206 | rowdict[col1] = col2 207 | finally: 208 | cur.close() 209 | 210 | return template('attributes.html', results=rowdict.items(), items=state.items(), refresh_token=urllib.parse.quote(state['refresh_token'])) 211 | 212 | # this will run the supplied SQL in the JDBC connector 213 | @app.get('/getattrjava') 214 | def get_attrjava(): 215 | # UNCOMMENT FOR DEBUGGING # print('ENTERING: get_attrjava') 216 | # UNCOMMENT FOR DEBUGGING # print('state right now: {}'.format(state)) 217 | rowdict = {} 218 | 219 | # create the SQL to pass 220 | # javaSQL = "\"" + snowflake_query + "\"" 221 | javaSQL = snowflake_query 222 | 223 | # create the classpath 224 | classpath = java_class + ":" + java_jdbcjar 225 | 226 | # UNCOMMENT FOR DEBUGGING # print("Classpath " + classpath + ", SQL " + javaSQL) 227 | 228 | result = subprocess.run(['java', '-cp', classpath, 'OAuthJDBCTest', state['access_token'], snowflake_accounturl, javaSQL, snowflake_role], stdout=subprocess.PIPE, universal_newlines=True, shell=False) 229 | 230 | # UNCOMMENT FOR DEBUGGING # print("This is the command result: ") 231 | # UNCOMMENT FOR DEBUGGING # print(result) 232 | # UNCOMMENT FOR DEBUGGING # print("The end of the command.") 233 | 234 | results = result.stdout.splitlines() 235 | 236 | # UNCOMMENT FOR DEBUGGING # print("these are the results: ") 237 | # UNCOMMENT FOR DEBUGGING # print(results) 238 | # UNCOMMENT FOR DEBUGGING # print("The end of the resultset.") 239 | 240 | for line in results: 241 | if len(line) != 0: 242 | (a, b) = line.split(',') 243 | #(a, b) = line.decode().split(',') 244 | col1 = a.strip() 245 | col2 = b.strip() 246 | rowdict[col1] = col2 247 | 248 | return template('attributes.html', results=rowdict.items(), items=state.items(), refresh_token=urllib.parse.quote(state['refresh_token'])) 249 | 250 | # performs a refresh of the access token using the refresch token 251 | @app.get('/refresh') 252 | def do_refresh(): 253 | # UNCOMMENT FOR DEBUGGING # print('ENTERING: do_refresh') 254 | # UNCOMMENT FOR DEBUGGING # print('state right now: {}'.format(state)) 255 | token = refresh_access_token(request.query.get('refresh_token')) 256 | state.update(token) 257 | 258 | # UNCOMMENT FOR DEBUGGING # print('state right now: {}'.format(state)) 259 | return template('token.html', items=state.items(), refresh_token=urllib.parse.quote(state['refresh_token'])) 260 | 261 | def get_token_code(code): 262 | # UNCOMMENT FOR DEBUGGING # print('ENTERING: get_token_code') 263 | # prepare POST parameters - encode them to urlencoded 264 | if do_pkce == "TRUE": 265 | data = urlencode({ 266 | 'grant_type': 'authorization_code', 267 | 'code': code, 268 | 'redirect_uri': redirect_uri, 269 | 'code_verifier': code_verifier 270 | }) 271 | else: 272 | data = urlencode({ 273 | 'grant_type': 'authorization_code', 274 | 'code': code, 275 | 'redirect_uri': redirect_uri 276 | }) 277 | data = data.encode('ascii') # data should be bytes 278 | # UNCOMMENT FOR DEBUGGING # print('data for token req: {}'.format(data)) 279 | resp_text = post_data(data, prepare_headers(), token_endpoint) 280 | 281 | # UNCOMMENT FOR DEBUGGING # print(resp_text) 282 | return json.loads(resp_text) 283 | 284 | # helper functions for the code above 285 | def refresh_access_token(refresh_token): 286 | # UNCOMMENT FOR DEBUGGING # print('ENTERING: refresh_access_token') 287 | # UNCOMMENT FOR DEBUGGING # print('state right now: {}'.format(state)) 288 | # prepare POST parameters - encode them to urlencoded 289 | data = urlencode({ 290 | 'grant_type': 'refresh_token', 291 | 'refresh_token': refresh_token, 292 | 'redirect_uri': redirect_uri 293 | }) 294 | data = data.encode('ascii') # data should be bytes 295 | resp_text = post_data(data, prepare_headers(), token_endpoint) 296 | 297 | # UNCOMMENT FOR DEBUGGING # print(resp_text) 298 | # UNCOMMENT FOR DEBUGGING # print('state right now: {}'.format(state)) 299 | return json.loads(resp_text) 300 | 301 | def prepare_headers(): 302 | # UNCOMMENT FOR DEBUGGING # print('ENTERING: prepare_headers') 303 | hdrs = { 304 | 'Authorization': 'Basic {}'.format(base64.b64encode('{}:{}'.format(client_id, client_secret).encode()).decode()), 305 | 'Content-type': 'application/x-www-form-urlencoded;charset=utf-8' 306 | } 307 | return hdrs 308 | 309 | def post_data(data, headers, url): 310 | # UNCOMMENT FOR DEBUGGING # print('ENTERING: post_data') 311 | # UNCOMMENT FOR DEBUGGING # print('post_data\nheaders:\n{}\ndata:\n{}\nurl:\n{}'.format(headers, data, url)) 312 | # UNCOMMENT FOR DEBUGGING # print('state right now: {}'.format(state)) 313 | req = urllib.request.Request(url, data=data, headers=headers) 314 | gcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) # avoid cert checking 315 | with urllib.request.urlopen(req, context=gcontext) as response: # perform POST request and read response 316 | rsp = response.read() 317 | return rsp.decode('utf-8') 318 | 319 | 320 | run(app, host='0.0.0.0', port=app_port) 321 | 322 | -------------------------------------------------------------------------------- /OAuthJDBCTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | ################################################################### 3 | ################################################################### 4 | ## The MIT License - SPDX short identifier: MIT ## 5 | ################################################################### 6 | ################################################################### 7 | # 8 | #Copyright 2019 @sanderiam & https://github.com/snowflakedb 9 | # 10 | #Permission is hereby granted, free of charge, to any person obtaining 11 | #a copy of this software and associated documentation files (the "Software"), 12 | #to deal in the Software without restriction, including without 13 | #limitation the rights to use, copy, modify, merge, publish, distribute, 14 | #sublicense, and/or sell copies of the Software, and to permit persons 15 | #to whom the Software is furnished to do so, subject to the following 16 | #conditions: 17 | # 18 | #The above copyright notice and this permission notice shall be 19 | #included in all copies or substantial portions of the Software. 20 | # 21 | #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | #EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | #MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | #IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | #CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | #TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | #SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | # 29 | 30 | # Please consider this script an example. 31 | # Do not use this in any production scenario 32 | 33 | */ 34 | 35 | import java.sql.Connection; 36 | import java.sql.DriverManager; 37 | import java.sql.ResultSet; 38 | import java.sql.ResultSetMetaData; 39 | import java.sql.SQLException; 40 | import java.sql.Statement; 41 | import java.util.Properties; 42 | 43 | public class OAuthJDBCTest { 44 | public static void main(String[] args) throws Exception 45 | { 46 | if (args.length != 4) 47 | { 48 | System.out.println("Please run with an OAuth Access Token, Snowflake account name and role!"); 49 | return; 50 | } 51 | 52 | 53 | // get connection 54 | Connection connection = getConnection(args[0], args[1], args[3]); 55 | if (connection == null) 56 | { 57 | return; 58 | } 59 | 60 | // use connection to run the statement 61 | Statement statement = connection.createStatement(); 62 | ResultSet resultSet = statement.executeQuery(args[2]); 63 | 64 | int rowIdx = 0; 65 | while (resultSet.next()) { 66 | System.out.println(resultSet.getString(1) + ", " + resultSet.getString(2)); 67 | } 68 | 69 | resultSet.close(); 70 | statement.close(); 71 | 72 | // System.out.println("Done creating JDBC connection\n"); 73 | connection.close(); 74 | } 75 | 76 | /** 77 | * Gets a connection via the OAuth Authenticator 78 | */ 79 | private static Connection getConnection(String oAuthAccessToken, String accountName, String role) 80 | throws SQLException 81 | { 82 | try 83 | { 84 | Class.forName("net.snowflake.client.jdbc.SnowflakeDriver"); 85 | } 86 | catch (ClassNotFoundException ex) 87 | { 88 | System.err.println("Driver not found"); 89 | return null; 90 | } 91 | 92 | String connectStr = "jdbc:snowflake://"; 93 | connectStr += accountName; 94 | 95 | // build connection properties. Note that username is optional 96 | // and role can be omitted 97 | Properties properties = new Properties(); 98 | properties.put("account", accountName); 99 | properties.put("authenticator", "OAUTH"); 100 | properties.put("token", oAuthAccessToken); 101 | properties.put("role", role); 102 | return DriverManager.getConnection(connectStr, properties); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /PKCE.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google Inc. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Utility functions for implementing Proof Key for Code Exchange (PKCE) by OAuth 17 | Public Clients 18 | See RFC7636. 19 | """ 20 | 21 | import base64 22 | import hashlib 23 | import os 24 | 25 | 26 | def code_verifier(n_bytes=64): 27 | """ 28 | Generates a 'code_verifier' as described in section 4.1 of RFC 7636. 29 | This is a 'high-entropy cryptographic random string' that will be 30 | impractical for an attacker to guess. 31 | Args: 32 | n_bytes: integer between 31 and 96, inclusive. default: 64 33 | number of bytes of entropy to include in verifier. 34 | Returns: 35 | Bytestring, representing urlsafe base64-encoded random data. 36 | """ 37 | verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=') 38 | # https://tools.ietf.org/html/rfc7636#section-4.1 39 | # minimum length of 43 characters and a maximum length of 128 characters. 40 | if len(verifier) < 43: 41 | raise ValueError("Verifier too short. n_bytes must be > 30.") 42 | elif len(verifier) > 128: 43 | raise ValueError("Verifier too long. n_bytes must be < 97.") 44 | else: 45 | return verifier 46 | 47 | 48 | def code_challenge(verifier): 49 | """ 50 | Creates a 'code_challenge' as described in section 4.2 of RFC 7636 51 | by taking the sha256 hash of the verifier and then urlsafe 52 | base64-encoding it. 53 | Args: 54 | verifier: bytestring, representing a code_verifier as generated by 55 | code_verifier(). 56 | Returns: 57 | Bytestring, representing a urlsafe base64-encoded sha256 hash digest, 58 | without '=' padding. 59 | """ 60 | digest = hashlib.sha256(verifier).digest() 61 | return base64.urlsafe_b64encode(digest).rstrip(b'=') 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth2.0-SampleApp 2 | Sample app to exercize the OAuth2.0 features of the Snowflake platform 3 | 4 | # Before Using 5 | You should read and follow all the steps here: https://docs.snowflake.net/manuals/user-guide/oauth-custom.html 6 | 7 | If you do not have either Snowflake or OAuth set up, then this app will not help you. 8 | 9 | Make sure you use a Snowflake user with a default warehouse which is set for auto-resume, and that rights to use that warehouse are specifically granted to the role which your OAuth scope will be invoking when you run the SQL. 10 | 11 | This was built and tested on ubuntu using Python 3.8.2 and AdoptOpenJDK build 11.0.8+10. Support for lower version of either is not assured, and there are modules and other Python 3 dependancies. Review the Python code carefully to ensure compatability with your testbed. 12 | 13 | # Steps to Use 14 | 15 | 1. Download all the files and put them in a directory of your choice. 16 | 2. Compile the OAuthJDBCTest.java code to create a OAuthJDBCTest.class file using a command similar to `javac -cp /path/to/the/OAuth2.0-SampleApp:/path/to/the/snowflake-jdbc-3.12.9.jar OAuthJDBCTest.java`. Note that the Snowflake JDBC jar can be located anywhere so long as you use a full path and name in the classpath. 17 | 3. Edit the OAuth2_0-SampleApp.ini for your Snowflake registered client following the instructions in the file's comments. 18 | 4. Launch the OAuth2_0-SampleApp.py file using `python3 ./OAuth2_0-SampleApp.py` or equivalent. If all is well, you will see `Bottle v0.12.18 server starting up (using WSGIRefServer())... 19 | Listening on http://0.0.0.0:8088/ 20 | Hit Ctrl-C to quit.` Note that if you're running the app in a container you will need to expose the port for the app to the host machine to use your regular browser. 21 | 5. Navigate to the app's URL with a browser and follow on screen instructions. 22 | 23 | # Expected Issues 24 | 1. When running your query using Python expect a warning similar to `/usr/lib/python3/dist-packages/urllib3/connectionpool.py:999: InsecureRequestWarning: Unverified HTTPS request is being made to host 'login.microsoftonline.com'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings` Since we won't have a proper certificate handling these requests in many cases while using this app, it's expected. Of course, if you're planning to use this as a model for anything more serious than testing OAuth features, then you should address this and put proper certs in place. 25 | 26 | Message me with issues. 27 | -------------------------------------------------------------------------------- /attributes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 36 | 37 | 38 | From here you can:
39 | % if not refresh_token == "null": 40 | 41 | % end 42 | 43 | 44 | 45 |
46 |
47 | Here are the query results:
48 | 49 | % for f1, f2 in results: 50 | 51 | 52 | 53 | 54 | % end 55 |
{{f1}}{{f2}}
56 |
57 |
58 | Current state of the token:
59 | 60 | % for k, v in items: 61 | 62 | 63 | 64 | 65 | % end 66 |
{{k}}{{v}}
67 | 68 | 69 | -------------------------------------------------------------------------------- /bad.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Something went wrong. In OAuth2.0 communications, a parameter called "state" is passed to the authorization endpoint and then back to the client. For some reason those values do not match. So we arrive here. The value that was passed was:
8 |
9 | {{state_parameter}}
10 |
11 | The value that came back was:
12 |
13 | {{returned_state_parameter}}
14 |
15 |
16 | The point of this exchange is to prevent CSRF (someone getting the wrong response intentionally or through error). Since we don't know why this happened here and we can't be sure it's not a bad guy, go back to the start and try again... 17 | 18 | 19 | -------------------------------------------------------------------------------- /logonform.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 40 | 41 | 42 |
43 |

44 | Credentials for you External OAuth Authorization Service
45 | This form should ONLY be used if you configured External OAuth. With External OAuth, you will be authenticating to a service which will then issue you an OAuth 2.0 Access Token which will grant you a session with Snowflake. These will be credentials for a user from the service (e.g. you AAD, Okta, Ping, or other IdP) which have already been configured with access in Snowflake. All other normal External OAuth ideas apply; including the scope determining the roles allowed. Please see Snowflake's documentation for detail. 46 |

47 | 48 |

49 |

50 | Username: 51 | Password:
52 |
53 |
54 | 55 |

56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29 | 30 | 31 | Welcome.
32 | This is a very simple application meant to show how OAuth2.0 may be used along with Snowflake. You can log in using your interacive authentication method of choice and then run a small, sample query. Each page you encounter will offer you all the options that make sense for what the applicatiln can do given what it has at that moment. 33 |
34 |
35 | From here you can: 36 |
37 | 38 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /token.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 35 | 36 | 37 | From here you can:
38 | % if not refresh_token == "null": 39 | 40 | % end 41 | 42 | 43 | 44 |
45 |
46 | Current state of the token:
47 | 48 | % for k, v in items: 49 | 50 | 51 | 52 | 53 | % end 54 |
{{k}}{{v}}
55 |
56 | 57 | 58 | --------------------------------------------------------------------------------