Skip to content

Commit e32f502

Browse files
author
Jack Zheng
committed
initial commit
0 parents  commit e32f502

9 files changed

+346
-0
lines changed

Dockerfile

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM ubuntu:14.04
2+
RUN apt-get update && apt-get install -y python-pip python-dev
3+
RUN mkdir -p /server
4+
ADD requirements.txt /server/requirements.txt
5+
WORKDIR /server
6+
RUN pip install -r requirements.txt
7+
8+
ADD . /server
9+
EXPOSE 8080
10+
CMD python app.py

README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# registry-oauth-server
2+
3+
## Quickstart
4+
5+
To build and start the containers, simply run the command
6+
7+
```
8+
./build.sh
9+
```
10+
After installation is finished, you should have a local registry running on *:5000*, and a local oauth server running on *:8080*.
11+
12+
```
13+
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
14+
915726d60e03 demo_oauth_server "/bin/sh -c 'python r" 8 minutes ago Up 8 minutes 0.0.0.0:8080->8080/tcp demo_oauth_server_1
15+
16c5c1cb2310 registry:2 "/bin/registry /etc/d" 2 hours ago Up 27 minutes 0.0.0.0:5000->5000/tcp demo_registry_1
16+
```
17+
18+
To see the oauth server in action, run the startdemo.sh script provided
19+
20+
```
21+
./startdemo.sh
22+
```
23+
24+
And you should see a detailed explaination of the oauth handshake process.
25+
26+
##CONFIGURATIONS
27+
28+
The following are a list of environment variables that has to be set for the oauth_server container to work properly.
29+
####SIGNING_KEY_PATH
30+
Specifies the path to the private key used to sign tokens. Must be a valid path inside the container.
31+
32+
####SIGNING_KEY_TYPE
33+
Specifies the type of key used to sign tokens. Can be either RSA or EC, defaults to RSA
34+
35+
####SIGNING_KEY_ALG
36+
Specifies the algorithm used to sign tokens. For RSA keys this should be set to RS256. For EC keys this should be set to ES256
37+
38+
####ISSUER
39+
Identifies who signed the token to the registry. Usually the FQDN of the OAuth Server is used. This configuration should match the REGISTRY_AUTH_TOKEN_ISSUER variable used to configure the registry.
40+
41+
####TOKEN_EXPIRATION
42+
Specifies the expiration time in seconds for tokens signed by this server. Defaults to 3600 seconds.
43+
44+
####TOKEN_TYPE
45+
Specifies the type of tokens signed by this server. Defaults to JWT

app.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from flask import Flask, request, jsonify
2+
from tokens import Token
3+
from auth import basic_auth_required
4+
5+
6+
app = Flask(__name__)
7+
8+
9+
def get_allowed_actions(user, actions):
10+
# determine what actions are allowed here
11+
return actions
12+
13+
14+
@app.route('/tokens')
15+
@basic_auth_required
16+
def tokens():
17+
service = request.args.get('service')
18+
scope = request.args.get('scope')
19+
if not scope:
20+
typ = ''
21+
name = ''
22+
actions = []
23+
else:
24+
params = scope.split(':')
25+
if len(params) != 3:
26+
return jsonify(error='Invalid scope parameter'), 400
27+
typ = params[0]
28+
name = params[1]
29+
actions = params[2].split(',')
30+
31+
authorized_actions = get_allowed_actions(request.user, actions)
32+
33+
token = Token(service, typ, name, authorized_actions)
34+
encoded_token = token.encode_token()
35+
36+
return jsonify(token=encoded_token)
37+
38+
39+
if __name__ == '__main__':
40+
app.run(host='0.0.0.0', port=8080)

auth.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from functools import wraps
2+
from flask import request, jsonify
3+
4+
5+
def check_auth(username, password):
6+
"""This function is called to check if a username /
7+
password combination is valid.
8+
"""
9+
request.user = username
10+
return True
11+
12+
13+
def authenticate():
14+
"""Sends a 401 response that enables basic auth"""
15+
return jsonify(error='Authentication required'), 401, \
16+
{'WWW-Authenticate': 'Basic realm="Login Required"'}
17+
18+
19+
def basic_auth_required(func):
20+
@wraps(func)
21+
def decorated(*args, **kwargs):
22+
auth = request.authorization
23+
if not auth or not check_auth(auth.username, auth.password):
24+
return authenticate()
25+
return func(*args, **kwargs)
26+
return decorated

build.sh

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
3+
if [ "$(uname)" == "Darwin" ]; then
4+
docker_ip="$(docker-machine ip default)"
5+
else
6+
docker_ip="0.0.0.0"
7+
fi
8+
9+
cat <<EOF > docker-compose.yml
10+
registry:
11+
restart: always
12+
image: registry:2
13+
ports:
14+
- 5000:5000
15+
environment:
16+
- REGISTRY_HTTP_TLS_CERTIFICATE=/certs/server.crt
17+
- REGISTRY_HTTP_TLS_KEY=/certs/server.key
18+
- REGISTRY_AUTH=token
19+
- REGISTRY_AUTH_TOKEN_REALM=http://${docker_ip}:8080/tokens
20+
- REGISTRY_AUTH_TOKEN_SERVICE=demo_registry
21+
- REGISTRY_AUTH_TOKEN_ISSUER=demo_oauth
22+
- REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/certs/server.crt
23+
volumes:
24+
- ./certs:/certs
25+
26+
27+
oauth_server:
28+
build: .
29+
ports:
30+
- 8080:8080
31+
volumes:
32+
- ./certs:/certs
33+
environment:
34+
- SIGNING_KEY_PATH=/certs/server.key
35+
- SIGNING_KEY_TYPE=RSA
36+
- SIGNING_KEY_ALG=RS256
37+
- ISSUER=demo_oauth
38+
- TOKEN_EXPIRATION=3600
39+
- TOKEN_TYPE=JWT
40+
EOF
41+
42+
docker-compose build
43+
docker-compose up -d

docker-compose.yml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
registry:
2+
restart: always
3+
image: registry:2
4+
ports:
5+
- 5000:5000
6+
environment:
7+
- REGISTRY_HTTP_TLS_CERTIFICATE=/certs/server.crt
8+
- REGISTRY_HTTP_TLS_KEY=/certs/server.key
9+
- REGISTRY_AUTH=token
10+
- REGISTRY_AUTH_TOKEN_REALM=http://192.168.99.100:8080/tokens
11+
- REGISTRY_AUTH_TOKEN_SERVICE=demo_registry
12+
- REGISTRY_AUTH_TOKEN_ISSUER=demo_oauth
13+
- REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/certs/server.crt
14+
volumes:
15+
- ./certs:/certs
16+
17+
18+
oauth_server:
19+
build: .
20+
ports:
21+
- 8080:8080
22+
volumes:
23+
- ./certs:/certs
24+
environment:
25+
- SIGNING_KEY_PATH=/certs/server.key
26+
- SIGNING_KEY_TYPE=RSA
27+
- SIGNING_KEY_ALG=RS256
28+
- ISSUER=demo_oauth
29+
- TOKEN_EXPIRATION=3600
30+
- TOKEN_TYPE=JWT

requirements.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Flask==0.10.1
2+
jose==1.0.0
3+
python-jose==0.5.5

startdemo.sh

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
3+
username="admin"
4+
pass="password"
5+
cred="${username}:${pass}"
6+
encoded_cred=`echo -n ${cred} | base64`
7+
service="demo_registry"
8+
9+
if [ "$(uname)" == "Darwin" ]; then
10+
docker_ip="$(docker-machine ip default)"
11+
else
12+
docker_ip="0.0.0.0"
13+
fi
14+
15+
16+
echo
17+
echo "curl https://${docker_ip}:5000/v2/_catalog"
18+
echo
19+
curl -skv "https://${docker_ip}:5000/v2/_catalog"
20+
read input
21+
clear
22+
23+
echo
24+
echo "curl http://${docker_ip}:8080/tokens?service=${service}&scope=registry:catalog:*"
25+
echo
26+
curl -sv -H "Authorization: Basic ${encoded_cred}" "http://${docker_ip}:8080/tokens?service=${service}&scope=registry:catalog:*"
27+
28+
token=`curl -s -H "Authorization: Basic ${encoded_cred}" "http://${docker_ip}:8080/tokens?service=${service}&scope=registry:catalog:*" | jq .token | tr -d '"'`
29+
read input
30+
clear
31+
32+
echo
33+
echo "curl \"https://${docker_ip}:5000/v2/_catalog\""
34+
curl -skv -H "Authorization: Bearer ${token}" "https://${docker_ip}:5000/v2/_catalog"
35+
echo

tokens.py

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import os
2+
import time
3+
import hashlib
4+
import base64
5+
import subprocess
6+
from jose import jwt
7+
8+
SIGNING_KEY_PATH = os.environ.get('SIGNING_KEY_PATH')
9+
SIGNING_KEY_TYPE = os.environ.get('SIGNING_KEY_TYPE')
10+
SIGNING_KEY_ALG = os.environ.get('SIGNING_KEY_ALG')
11+
SIGNING_KEY = open(SIGNING_KEY_PATH).read()
12+
13+
ISSUER = os.environ.get('ISSUER')
14+
TOKEN_EXPIRATION = os.environ.get('TOKEN_EXPIRATION')
15+
TOKEN_TYPE = os.environ.get('TOKEN_TYPE')
16+
17+
18+
def run_command(command):
19+
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
20+
return process.communicate()
21+
22+
23+
def key_id_encode(the_bytes):
24+
source = base64.b32encode(the_bytes).rstrip("=")
25+
result = []
26+
for i in xrange(0, len(source), 4):
27+
start = i
28+
end = start+4
29+
result.append(source[start:end])
30+
return ":".join(result)
31+
32+
33+
def kid_from_crypto_key(private_key_path, key_type='EC'):
34+
"""
35+
python implementation of
36+
https://github.com/jlhawn/libtrust/blob/master/util.go#L192
37+
returns a distinct identifier which is unique to
38+
the public key derived from this private key.
39+
The format generated by this library is a base32 encoding of a 240 bit
40+
hash of the public key data divided into 12 groups like so:
41+
ABCD:EFGH:IJKL:MNOP:QRST:UVWX:YZ23:4567:ABCD:EFGH:IJKL:MNOP
42+
"""
43+
algorithm = hashlib.sha256()
44+
if key_type == 'EC':
45+
der, msg = run_command(['openssl', 'ec', '-in', private_key_path,
46+
'-pubout', '-outform', 'DER'])
47+
48+
elif key_type == 'RSA':
49+
der, msg = run_command(['openssl', 'rsa', '-in', private_key_path,
50+
'-pubout', '-outform', 'DER'])
51+
52+
else:
53+
raise Exception("Key type not supported")
54+
55+
if not der:
56+
raise Exception(msg)
57+
58+
algorithm.update(der)
59+
return key_id_encode(algorithm.digest()[:30])
60+
61+
62+
class Token(object):
63+
def __init__(self, service, access_type="", access_name="",
64+
access_actions=None, subject=''):
65+
if access_actions is None:
66+
access_actions = []
67+
68+
self.issuer = ISSUER
69+
self.signing_key = SIGNING_KEY
70+
self.signing_key_path = SIGNING_KEY_PATH
71+
self.signing_key_type = SIGNING_KEY_TYPE
72+
self.signing_key_alg = SIGNING_KEY_ALG
73+
self.token_expiration = TOKEN_EXPIRATION
74+
self.token_type = TOKEN_TYPE
75+
self.header = {
76+
'typ': self.token_type,
77+
'alg': self.signing_key_alg,
78+
'kid': kid_from_crypto_key(self.signing_key_path,
79+
self.signing_key_type)
80+
}
81+
self.claim = {
82+
'iss': self.issuer,
83+
'sub': subject,
84+
'aud': service,
85+
'exp': int(time.time()) + int(self.token_expiration),
86+
'nbf': int(time.time()) - 30,
87+
'iat': int(time.time()),
88+
'access': [
89+
{
90+
'type': access_type,
91+
'name': access_name,
92+
'actions': access_actions
93+
}
94+
]
95+
}
96+
97+
def set_header(self, header):
98+
self.header = header
99+
100+
def get_header(self):
101+
return self.header
102+
103+
def set_claim(self, claim):
104+
self.claim = claim
105+
106+
def get_claim(self):
107+
return self.claim
108+
109+
def encode_token(self):
110+
token = jwt.encode(self.claim, self.signing_key,
111+
algorithm=self.signing_key_alg,
112+
headers=self.header)
113+
114+
return token

0 commit comments

Comments
 (0)