├── .Dockerignore ├── .gitignore ├── .travis.yml ├── CONDUCT.md ├── DESIGN.md ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── authplz.yml ├── cmd ├── authplz │ └── main.go └── examples │ └── cli │ └── main.go ├── gencert.sh ├── heroku.yml ├── lib ├── api │ ├── messages.go │ ├── paginate.go │ └── token.go ├── app │ ├── app_test.go │ └── server.go ├── appcontext │ ├── bind.go │ ├── bind_2fa.go │ ├── bind_flash.go │ ├── bind_recovery.go │ ├── context.go │ ├── helpers.go │ └── sudo.go ├── config │ ├── config.go │ ├── config_test.go │ ├── mailer.go │ ├── oauth.go │ └── tls.go ├── controllers │ ├── datastore │ │ ├── actiontoken.go │ │ ├── auditevent.go │ │ ├── backuptoken.go │ │ ├── datastore.go │ │ ├── datastore_test.go │ │ ├── fidotoken.go │ │ ├── oauth2 │ │ │ ├── access_token.go │ │ │ ├── authorize_code.go │ │ │ ├── client.go │ │ │ ├── oauth.go │ │ │ ├── refresh_token.go │ │ │ ├── request.go │ │ │ └── session.go │ │ ├── oauthstore_test.go │ │ ├── totptoken.go │ │ └── user.go │ ├── mailer │ │ ├── drivers │ │ │ ├── logger.go │ │ │ ├── mailgun.go │ │ │ └── mailgun_test.go │ │ ├── mailer.go │ │ ├── mailer_interface.go │ │ └── mailer_test.go │ └── token │ │ ├── token.go │ │ ├── token_interface.go │ │ └── token_test.go ├── events │ └── events.go ├── modules │ ├── 2fa │ │ ├── backup │ │ │ ├── backup.go │ │ │ ├── backup_api.go │ │ │ ├── backup_interface.go │ │ │ └── backup_test.go │ │ ├── totp │ │ │ ├── totp.go │ │ │ ├── totp_api.go │ │ │ ├── totp_interface.go │ │ │ └── totp_test.go │ │ └── u2f │ │ │ ├── u2f.go │ │ │ ├── u2f_api.go │ │ │ ├── u2f_interface.go │ │ │ └── u2f_test.go │ ├── audit │ │ ├── audit.go │ │ ├── audit_api.go │ │ ├── audit_interface.go │ │ └── audit_test.go │ ├── core │ │ ├── core.go │ │ ├── core_api.go │ │ ├── core_api_test.go │ │ ├── core_interface.go │ │ ├── core_test.go │ │ ├── handlers.go │ │ └── plugins.go │ ├── oauth │ │ ├── fosite_adaptor.go │ │ ├── fosite_wrappers.go │ │ ├── helpers.go │ │ ├── oauth.go │ │ ├── oauth_api.go │ │ ├── oauth_api_test.go │ │ ├── oauth_interface.go │ │ ├── oauth_test.go │ │ └── session.go │ └── user │ │ ├── user.go │ │ ├── user_api.go │ │ ├── user_api_test.go │ │ ├── user_errors.go │ │ ├── user_interface.go │ │ └── user_test.go ├── plugins │ └── ratelimit │ │ └── ratelimit.go └── test │ ├── helpers.go │ ├── mockemitter.go │ └── testclient.go ├── lock.json ├── swagger.yml └── templates ├── activation.tmpl ├── loginnotice.tmpl ├── passwordchanged.tmpl ├── passwordreset.tmpl └── unlock.tmpl /.Dockerignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /vendor 3 | /_vendor* 4 | setenv.sh 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | authplz 26 | setenv.sh 27 | *.key 28 | *.pem 29 | *.crt 30 | static/ 31 | 32 | vendor/ 33 | .vscode 34 | 35 | authplz-ui 36 | authplz-ui/ 37 | authplz-app 38 | 39 | cli 40 | service 41 | *_mocks.go 42 | _vendor* 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 1.8 3 | services: 4 | - postgresql: 9.4 5 | install: 6 | - make install 7 | before_script: 8 | - psql -c "ALTER ROLE postgres WITH PASSWORD 'postgres';" -U postgres 9 | script: 10 | - make test cross && tar -cvf build/templates.tgz templates/* 11 | cache: 12 | directories: 13 | - vendor 14 | notifications: 15 | email: false 16 | deploy: 17 | provider: releases 18 | api_key: 19 | secure: rp5T6PwFIQscLWwzSX6ldv1HFCgNA0hT6yOj7/BX7lMfCP97YEO4Q0kX7gG2aJgefCCLxHL0OM8l3Ln+tcqmUC8rUIPEbhIPe0A96LWGcmt2KMcEIJxp+5/VvnziHe2k1Mh6b9hrsaVKSnD7qBlUEJUfu57XDau6ea/vncLGNX+IqyTFeAIs1ffv9xlvqQkIeCuSmgCD3ZsuKO8qPdFGg0fHEiIfuzg8E2GtKLHBABA8CEytAnuLrufEkUCG+F91cQG2KgkfHSUIWscDh2u5HWPD06ArVDO9nLRDPeZFkMyvFPw57VaFkaR31PcgCVX7xdIbqxR09QChHdkQD3mMCQuH2W8TUZRKAcqvuXeT1e5DfvFNfriR9vG/Iap20uy3ByD9VTkl4F5GkQFxTeQ2SjzD6vU+SCZxSH7YZznvXlljmI9Rvg4dRR5xDK7OvjdIyUykBNFdrmt71CSXXkbspAL6E40bVEqzWHoYATFCJZ1vEAg8jPYimD6NlMPes9uz/mfv1WOQl4i9g0+aJ8PZwcL9IfDLx5DkViocWLcAuLoRm04g1TcTGCN4U7/8Rvp4aKg1wzoJ6Avh45tHMzkTI73njE5LVfn40AaU4FsdXhfRH2kp1d/s5PG37oSlGYX9i/8gH2IxogO/KU2PPKp3h71EdavPHGo0pOXeM4H9l5c= 20 | file_glob: true 21 | file: build/* 22 | skip_cleanup: true 23 | on: 24 | repo: authplz/authplz-core 25 | branch: master 26 | tags: true -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [ryankurte@gmail.com](ryankurte@gmail.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | Design notes and questions for AuthPlz. 4 | 5 | 6 | ## Overview 7 | 8 | - All endpoints should return 400 bad request if required parameters or fields in the request body are not issued 9 | - Authentication endpoints will return 200 success/201 partial or 403 unauthorized 10 | - Internal errors will result in a 401 internal error with no (or a generic) error message to avoid leaking internal 11 | - Other endpoints will return JSON formatted API messages 12 | 13 | ### Modules 14 | 15 | Modules consist of a set of interfaces, defining the dependencies of the module, a controller that uses these interfaces to implement the functionality of the module, and an api that wraps the controller in HTTP endpoints. 16 | 17 | All data structures returned from controllers should be safe for API use (ie. no internal structures may be returned, explicitly wrap / translate everything). 18 | 19 | 20 | ## Flows 21 | 22 | ### Account Creation 23 | 24 | 1. post username, email, password to /api/users/create 25 | 2. server sends account activation email 26 | 3. execute activation link 27 | 4. server sends account activated email 28 | 29 | 30 | ### Basic Login 31 | 32 | 1. post email, password to /api/login 33 | 2. server responds with 200 success, 201 partial (2fa) or 403 unauthorized 34 | 35 | 36 | ### Account Unlock 37 | 38 | 1. post email, password to /api/login 39 | 2. server responds with 403 unauthorized, sends unlock email to registered address, caches credentials 40 | 3. get /api/flash returns "Account locked" 41 | 4. user clicks unlock link to /api/action?token=TOKEN 42 | 5. server redirects to login page 43 | 6. post email, password to /api/login (unless credentials are already cached) 44 | 7. server executes unlock token 45 | 46 | Seems like this could be more efficient / remove the need for the second login if the user clicks the unlock link with a partially formed session (valid username and password). 47 | 48 | 49 | ### Password Change 50 | 51 | 1. user logs in as above 52 | 2. user submits old, new passwords to /api/users/account 53 | 3. server validates, responds with 200 success or 400 bad request 54 | 55 | 56 | ### U2F enrolment 57 | 58 | 1. user logs in as above 59 | 2. user submits token name to /api/u2f/register 60 | 3. server responds with registration challenge 61 | 4. browser executes challenge, posts response 62 | 5. server validates registration response 63 | 6. server responds with 200 success or 401 error 64 | 65 | 66 | ### U2F Login 67 | 68 | 1. post email, password to /api/login 69 | 2. server responds with 201 partial (2fa) and available factors object ({u2f: true}) 70 | 3. browser fetches challenge from /api/u2f/authenticate 71 | 4. browser executes challenge, posts response 72 | 5. server responds with 200 success or 403 unauthorized 73 | 74 | ### TOTP enrolment 75 | 76 | 1. user logs in as above 77 | 2. user submits token name to /api/totp/enrol 78 | 3. server responds with registration challenge (string and image) 79 | 4. user loads totp onto device, posts a valid code 80 | 5. server validates registration response 81 | 6. server responds with 200 success or 401 error 82 | 83 | 84 | ### TOTP Login 85 | 86 | 1. post email, password to /api/login 87 | 2. server responds with 201 partial (2fa) and available factors object ({totp: true}) 88 | 3. user gets code from totp app 89 | 4. browser posts code to /api/totp/authenticate 90 | 5. server responds with 200 success or 403 unauthorized 91 | 92 | ### Password Reset 93 | 94 | 1. post email account to /api/recovery 95 | 2. server sends recovery token to user email 96 | 3. token submitted to /api/recovery (could be /api/token, but different process required so easier to split) 97 | 4. if 2fa, require 2fa to validate recovery session. If lost, sms or recovery codes. 98 | 5. user submits new password to /api/reset 99 | 6. server responds 200 success or 400 bad request 100 | 7. server sends alert email to user 101 | 102 | This requires that all stages be undertaken from the same session. Backup codes are treated just another 2fa provider. 103 | 104 | 105 | ### OAuth Clients 106 | 107 | A variety of clients can be enrolled based on user account priviledges. Admins can enrol all OAuth client types, users can enrol Client Credential (for end devices) and Implicit (no secret storage) client types. 108 | Allowed authorizations for each account type can be set in the configuration file. 109 | 110 | #### Authorisation Code (Explicit) Grant 111 | For trusted services, created by administrators, available to all users. 112 | 113 | #### Authorisation Code (Implicit) Grant 114 | For services that do not have secret storage, created by and available to individual users. 115 | 116 | #### Client Credentials Grant 117 | For end devices, created by and available to individual users. 118 | 119 | #### Introspection 120 | Explicit grants can be provided with the "introspection" scope, allowing introspection of other tokens using these credentials. 121 | This allows trusted services to evaluate the validity of credentials for broker-like behaviour. 122 | 123 | 124 | #### Refresh Token Grant 125 | 126 | Allows tokens to be refreshed / reissued. Available with both Authorization Code grant types. 127 | 128 | ## Questions 129 | 130 | - How can you enrol / remove tokens, what is required? 131 | - How do plugins require further login validation (ie. "this doesn't look right, click token in email to validate")? 132 | - How do we run multiple OAuth schemes for different clients? 133 | - Guess user interaction is going to be important here as to what is granted 134 | 135 | ## Discussions 136 | 137 | What if instead of imposing a security level on users, we informed them and let them pick? 138 | Users could then be given a security score on their account dashboard to gamify improving it. 139 | For example: 140 | - You only have password set, password resets and account recovery will currently require only your email address, register a phone number or 2fa token to improve this 141 | - Good work registering 2fa! Password resets will now require this 2fa token. For account recovery purposes you must now either register a phone number or create recovery codes 142 | 143 | Other ideas: 144 | 145 | - Testing that users have recovery keys etc to keep people in practice. Users could be (optionally) prompted to enter a backup key name at a given interval, on failure given the option of regenerating backup keys. 146 | 147 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | MAINTAINER Ryan Kurte 3 | LABEL Description="Authentication and User Management Microservice" 4 | 5 | # Build app 6 | COPY . /go/src/github.com/authplz/authplz-core 7 | WORKDIR /go/src/github.com/authplz/authplz-core 8 | RUN make install 9 | RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 make build 10 | 11 | # Build UI 12 | FROM node:latest 13 | RUN git clone https://github.com/authplz/authplz-ui.git 14 | WORKDIR /authplz-ui 15 | RUN npm install 16 | RUN npm run build:prod 17 | 18 | # Build release image 19 | FROM alpine:latest 20 | WORKDIR /app/ 21 | RUN adduser -D authplz 22 | RUN chown -R authplz:authplz /app 23 | RUN chmod -R o+rx /app 24 | 25 | COPY --from=0 /go/src/github.com/authplz/authplz-core/authplz . 26 | COPY --from=0 /go/src/github.com/authplz/authplz-core/authplz.yml config/authplz.yml 27 | COPY --from=0 /go/src/github.com/authplz/authplz-core/templates templates 28 | 29 | COPY --from=1 /authplz-ui/build static 30 | 31 | ENV HOST=0.0.0.0 32 | ENV PORT=9000 33 | ENV EXTERNAL_ADDRESS=http://authplz.local 34 | 35 | #USER authplz 36 | CMD ["/app/authplz -c /app/config/authplz.yml"] 37 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/NebulousLabs/entropy-mnemonics" 31 | 32 | [[constraint]] 33 | name = "github.com/asaskevich/govalidator" 34 | version = "8.0.0" 35 | 36 | [[constraint]] 37 | name = "github.com/dgrijalva/jwt-go" 38 | version = "3.1.0" 39 | 40 | [[constraint]] 41 | name = "github.com/gocraft/web" 42 | version = "1.1.0" 43 | 44 | [[constraint]] 45 | name = "github.com/golang/mock" 46 | version = "1.0.0" 47 | 48 | [[constraint]] 49 | name = "github.com/gorilla/context" 50 | version = "1.1.0" 51 | 52 | [[constraint]] 53 | name = "github.com/gorilla/sessions" 54 | version = "1.1.0" 55 | 56 | [[constraint]] 57 | name = "github.com/jessevdk/go-flags" 58 | version = "1.3.0" 59 | 60 | [[constraint]] 61 | name = "github.com/jinzhu/gorm" 62 | version = "1.0.0" 63 | 64 | [[constraint]] 65 | name = "github.com/ory/fosite" 66 | version = "0.12.0" 67 | 68 | [[constraint]] 69 | name = "github.com/pkg/errors" 70 | version = "0.8.0" 71 | 72 | [[constraint]] 73 | name = "github.com/pquerna/otp" 74 | version = "1.0.0" 75 | 76 | [[constraint]] 77 | name = "github.com/ryankurte/go-async" 78 | version = "1.0.0" 79 | 80 | [[constraint]] 81 | name = "github.com/ryankurte/go-structparse" 82 | version = "1.1.1" 83 | 84 | [[constraint]] 85 | name = "github.com/ryankurte/go-u2f" 86 | version = "0.1.4" 87 | 88 | [[constraint]] 89 | name = "github.com/satori/go.uuid" 90 | version = "1.2.0" 91 | 92 | [[constraint]] 93 | name = "github.com/stretchr/testify" 94 | version = "1.2.0" 95 | 96 | [[constraint]] 97 | branch = "master" 98 | name = "golang.org/x/crypto" 99 | 100 | [[constraint]] 101 | branch = "master" 102 | name = "golang.org/x/oauth2" 103 | 104 | [[constraint]] 105 | name = "gopkg.in/mailgun/mailgun-go.v1" 106 | version = "1.1.0" 107 | 108 | [[constraint]] 109 | branch = "v2" 110 | name = "gopkg.in/yaml.v2" 111 | 112 | [prune] 113 | go-tests = true 114 | unused-packages = true 115 | 116 | [metadata.heroku] 117 | root-package = "github.com/authplz/authplz-core" 118 | go-version = "1.9.2" 119 | install = [ "./cmd/authplz/..." ] 120 | 121 | [[constraint]] 122 | name = "github.com/nbutton23/zxcvbn-go" 123 | version = "0.1.0" 124 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Helpers for AuthPlz development 2 | 3 | VER=$(shell git describe --dirty) 4 | ARGS= -ldflags "-X main.version=$(VER)" 5 | 6 | # Core Functions 7 | 8 | default: build 9 | 10 | dir: 11 | @mkdir -p build 12 | 13 | # Install dependencies 14 | install: 15 | go get -u github.com/golang/lint/golint 16 | go get -u github.com/golang/dep/cmd/dep 17 | go get github.com/golang/mock/gomock 18 | go get github.com/golang/mock/mockgen 19 | 20 | go get -u github.com/jteeuwen/go-bindata/... 21 | go get -u golang.org/x/oauth2 22 | 23 | dep ensure 24 | 25 | # Build backend and frontend components 26 | build: 27 | go build $(ARGS) ./cmd/authplz 28 | 29 | # Run application 30 | run: build 31 | ./authplz -c authplz.yml 32 | 33 | # Test application 34 | test: mocks 35 | @go test -p=1 ./lib/... 36 | 37 | mocks: 38 | mockgen -source lib/modules/core/core_interface.go -destination lib/modules/core/core_mocks.go -package core 39 | mockgen -source lib/events/events.go -destination lib/events/event_mocks.go -package events 40 | mockgen -source lib/controllers/mailer/mailer_interface.go -destination lib/controllers/mailer/mailer_mocks.go -package mailer 41 | mockgen -source lib/modules/user/user_interface.go -destination lib/modules/user/user_mocks.go -package user 42 | mockgen -source lib/modules/2fa/backup/backup_interface.go -destination lib/modules/2fa/backup/backup_mocks.go -package backup 43 | mockgen -source lib/modules/oauth/oauth_interface.go -destination lib/modules/oauth/oauth_mocks.go -package oauth 44 | 45 | cross: dir 46 | GOOS=linux GOARCH=amd64 go build $(ARGS) -o build/authplz-linux-amd64 ./cmd/authplz 47 | GOOS=linux GOARCH=arm go build $(ARGS) -o build/authplz-linux-armhf ./cmd/authplz 48 | GOOS=darwin GOARCH=amd64 go build $(ARGS) -o build/authplz-linux-armhf ./cmd/authplz 49 | GOOS=windows GOARCH=amd64 go build $(ARGS) -o build/authplz-windows-amd64 ./cmd/authplz 50 | 51 | # Frontend components now in authplz-ui package 52 | 53 | # Utilities 54 | 55 | lint: 56 | golint ./lib/... 57 | 58 | format: 59 | gofmt -w -s ./lib/... 60 | 61 | validate: 62 | swagger validate swagger.yml 63 | 64 | coverage: 65 | go test -p=1 -cover ./lib/... 66 | 67 | checks: format lint coverage 68 | 69 | # Container control 70 | 71 | docker: 72 | docker build -t authplz/authplz-core . 73 | 74 | # Build containerized development environment 75 | build-env: clean-env 76 | docker create --name ap-pg -p 5432:5432 postgres 77 | docker start ap-pg 78 | sleep 3 79 | docker run -it --rm --link ap-pg:ap-pg postgres createuser -h ap-pg -U postgres test -drsi 80 | docker run -it --rm --link ap-pg:ap-pg postgres createdb -h ap-pg -U postgres -O test test 81 | docker run -it --rm --link ap-pg:ap-pg postgres psql -h ap-pg -U postgres -c "GRANT ALL ON DATABASE test TO test;" 82 | docker run -it --rm --link ap-pg:ap-pg postgres psql -h ap-pg -U postgres -c "ALTER USER test WITH PASSWORD 'test';" 83 | 84 | 85 | # Start development environment 86 | start-env: 87 | docker start ap-pg 88 | 89 | # Stop development environment 90 | stop-env: 91 | docker stop ap-pg 92 | 93 | # Clean up development environment 94 | clean-env: stop-env 95 | docker rm ap-pg 96 | 97 | # Lanch interactive PSQL connected to development db 98 | psql: 99 | docker run -it --rm --link ap-pg:ap-pg postgres psql -h ap-pg -U postgres 100 | 101 | 102 | .PHONY: start-env stop-env clean-env frontend test dir 103 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: authplz -c ./heroku.yml -p "" 2 | -------------------------------------------------------------------------------- /authplz.yml: -------------------------------------------------------------------------------- 1 | # AuthPlz Example Configuration 2 | # $VARIABLES are loaded from the environment with the prefix specified on the command line 3 | 4 | # User friendly application name 5 | name: AuthPlz Example 6 | 7 | # Server binding configuration 8 | bind-address: $HOST 9 | bind-port: $PORT 10 | 11 | # External application address (required for reverse proxying) 12 | external-address: $EXTERNAL_ADDRESS 13 | 14 | # Database connection string 15 | database: $DATABASE_URL 16 | 17 | disable-web-security: true 18 | 19 | # Allowed origins for API requests 20 | # This is automatically set to bind-address:bind-port (or external-address if set) but can be overridden here if required 21 | allowed-origins: 22 | - https://localhost:3000 23 | - http://localhost:3000 24 | - http://192.168.1.28:9000 25 | 26 | # Secrets 27 | cookie-secret: $COOKIE_SECRET 28 | token-secret: $TOKEN_SECRET 29 | 30 | # TLS configuration 31 | tls: 32 | # cert: server.pem 33 | # key: server.key 34 | disabled: true 35 | 36 | # Template and static file directories 37 | static-dir: ./static 38 | template-dir: ./templates 39 | 40 | # OAuth (Client) Configuration 41 | # Scopes define what resources an admin or users client can grant access to. 42 | # These are heirachicle and are split by '.' (eg. public includes public.read) 43 | # Grants correspond to OAuth grant types that clients can utilise 44 | # Allowed responses defines what responses are allowed for OAuth clients 45 | oauth: 46 | secret: $OAUTH_SECRET 47 | admin: 48 | scopes: ["public.read", "public.write", "private.read", "private.write", "introspect", "offline"] 49 | grants: ["authorization_code", "implicit", "refresh_token", "client_credentials"] 50 | user: 51 | scopes: ["public.read", "public.write", "private.read", "private.write", "offline"] 52 | grants: ["authorization_code", "implicit", "refresh_token"] 53 | allowed-responses: ["code", "token", "id_token"] 54 | 55 | # Mailer configuration 56 | mailer: 57 | driver: mailgun 58 | options: 59 | domain: $MG_DOMAIN 60 | address: $MG_ADDRESS 61 | key: $MG_APIKEY 62 | secret: $MG_PRIKEY 63 | 64 | -------------------------------------------------------------------------------- /cmd/authplz/main.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Server entry point 3 | * 4 | * AuthPlz Project (https://github.com/authplz/authplz-core) 5 | * Copyright 2018 Ryan Kurte 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "log" 12 | "os" 13 | 14 | "github.com/authplz/authplz-core/lib/app" 15 | "github.com/authplz/authplz-core/lib/config" 16 | ) 17 | 18 | var version string 19 | 20 | func main() { 21 | 22 | log.Printf("AuthPlz (version: %s)", version) 23 | 24 | // Load configuration 25 | c, err := config.GetConfig() 26 | if err != nil { 27 | log.Printf("Error loading config: %s", err) 28 | os.Exit(-1) 29 | } 30 | 31 | // Create server instance 32 | server, err := app.NewServer(*c) 33 | if err != nil { 34 | log.Printf("%s", err) 35 | os.Exit(-2) 36 | } 37 | 38 | // Launch server 39 | server.Start() 40 | } 41 | -------------------------------------------------------------------------------- /cmd/examples/cli/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * AuthPlz Command Line Application Example 3 | * Demonstrates authentication of command line applications using AuthPlz 4 | * 5 | * AuthPlz Project (https://github.com/ryankurte/AuthPlz) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package main 10 | 11 | import ( 12 | "crypto/rand" 13 | "fmt" 14 | "net/http" 15 | "net/url" 16 | "os" 17 | 18 | // "golang.org/x/oauth2" 19 | 20 | "github.com/jessevdk/go-flags" 21 | //"time" 22 | "encoding/base64" 23 | ) 24 | 25 | type Config struct { 26 | OAuthAddress string `short:"o" long:"oauth-address" description:"Set authorization server endpoint" default:"https://localhost:9000/api/oauth/auth"` 27 | BindAddress string `short:"a" long:"bind-address" description:"Set cli bind address" default:"localhost:9002"` 28 | ClientID string `short:"i" long:"client-id" description:"OAuth2 Client ID"` 29 | ClientSecret string `short:"s" long:"client-secret" description:"OAuth2 Client Secret" default-mask:"-"` 30 | TLSCert string `short:"c" long:"tls-cert" description:"TLS Certificate file" default:"./client.crt"` 31 | TLSKey string `short:"k" long:"tls-key" description:"TLS Key file" default:"./client.key"` 32 | } 33 | 34 | func getOAuthImplicitLink(c *Config) string { 35 | 36 | b := make([]byte, 32) 37 | rand.Read(b) 38 | 39 | v := url.Values{} 40 | 41 | v.Set("client_id", c.ClientID) 42 | v.Set("response_type", "token") 43 | v.Set("grant_type", "implicit") 44 | v.Set("redirect_uri", fmt.Sprintf("https://%s", c.BindAddress)) 45 | v.Set("scope", "public.read private.read") 46 | v.Set("state", base64.StdEncoding.EncodeToString(b)) 47 | 48 | return fmt.Sprintf("%s?%s", c.OAuthAddress, v.Encode()) 49 | } 50 | 51 | func getOauthExplicitLink(c *Config) string { 52 | b := make([]byte, 32) 53 | rand.Read(b) 54 | 55 | v := url.Values{} 56 | 57 | v.Set("client_id", c.ClientID) 58 | v.Set("response_type", "code") 59 | v.Set("redirect_uri", fmt.Sprintf("https://%s", c.BindAddress)) 60 | v.Set("scope", "public.read private.read") 61 | v.Set("state", base64.StdEncoding.EncodeToString(b)) 62 | 63 | return fmt.Sprintf("%s?%s", c.OAuthAddress, v.Encode()) 64 | } 65 | 66 | func newHandler(origin string, ch chan *http.Request) func(w http.ResponseWriter, r *http.Request) { 67 | 68 | return func(w http.ResponseWriter, r *http.Request) { 69 | 70 | // Set access control to allow auth site queries 71 | w.Header().Set("access-control-allow-origin", origin) 72 | w.Header().Set("access-control-allow-credentials", "true") 73 | 74 | err := r.URL.Query().Get("error") 75 | disc := r.URL.Query().Get("error_description") 76 | if err != "" { 77 | fmt.Printf("OAuth error: %s (%s)\n", err, disc) 78 | w.WriteHeader(http.StatusOK) 79 | return 80 | } 81 | 82 | w.WriteHeader(http.StatusOK) 83 | 84 | fmt.Printf("OAuth response: %+v\n", r.URL.Query()) 85 | } 86 | } 87 | 88 | func main() { 89 | var c Config 90 | 91 | fmt.Println("AuthPlz Command Line Application Example") 92 | 93 | // Load command line args 94 | _, err := flags.Parse(&c) 95 | if err != nil { 96 | os.Exit(-1) 97 | } 98 | 99 | remote, _ := url.Parse(c.OAuthAddress) 100 | origin := fmt.Sprintf("%s://%s", remote.Scheme, remote.Host) 101 | 102 | // Start local HTTP server (for OAuth redirect) 103 | ch := make(chan *http.Request) 104 | http.HandleFunc("/", newHandler(origin, ch)) 105 | go http.ListenAndServeTLS(c.BindAddress, c.TLSCert, c.TLSKey, nil) 106 | 107 | // Print auth link for user to click 108 | link := getOauthExplicitLink(&c) 109 | 110 | fmt.Println("Click the following link to authorize the application") 111 | fmt.Println(link) 112 | 113 | // Await token from redirect endpoint (with timeout) 114 | for { 115 | select { 116 | case resp, ok := <-ch: 117 | if !ok { 118 | fmt.Printf("Channel closed error\n") 119 | break 120 | } 121 | fmt.Printf("Received: %+v\n", resp) 122 | 123 | /* 124 | case <-time.After(time.Second * 30): 125 | fmt.Printf("Timeout awaiting application authorization\n") 126 | */ 127 | } 128 | } 129 | 130 | // Save / use token? 131 | 132 | } 133 | -------------------------------------------------------------------------------- /gencert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Key considerations for algorithm "RSA" ≥ 2048-bit 4 | openssl genrsa -out server.key 2048 5 | 6 | # Key considerations for algorithm "ECDSA" ≥ secp384r1 7 | # List ECDSA the supported curves (openssl ecparam -list_curves) 8 | openssl ecparam -genkey -name secp384r1 -out server.key 9 | 10 | # Generate self-signed(x509) public key (PEM-encodings .pem|.crt) based on the private (.key) 11 | openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650 12 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | # AuthPlz Example Configuration 2 | # $VARIABLES are loaded from the environment with the prefix specified on the command line 3 | 4 | # User friendly application name 5 | name: authplz.github.io 6 | 7 | # Server binding configuration 8 | bind-address: 0.0.0.0 9 | bind-port: $PORT 10 | 11 | # External application address (required for reverse proxying) 12 | external-address: https://authplz.github.io 13 | 14 | # Database connection string 15 | database: $DATABASE_URL 16 | 17 | disable-web-security: true 18 | 19 | # Allowed origins for API requests 20 | # This is automatically set to bind-address:bind-port (or external-address if set) but can be overridden here if required 21 | allowed-origins: 22 | - https://localhost:3000 23 | - https://authplz.github.io 24 | 25 | # Secrets 26 | cookie-secret: $COOKIE_SECRET 27 | token-secret: $TOKEN_SECRET 28 | 29 | # TLS configuration 30 | tls: 31 | # cert: server.pem 32 | # key: server.key 33 | disabled: true 34 | 35 | # Template and static file directories 36 | template-dir: ./templates 37 | 38 | # OAuth (Client) Configuration 39 | # Scopes define what resources an admin or users client can grant access to. 40 | # These are heirachicle and are split by '.' (eg. public includes public.read) 41 | # Grants correspond to OAuth grant types that clients can utilise 42 | # Allowed responses defines what responses are allowed for OAuth clients 43 | oauth: 44 | secret: $OAUTH_SECRET 45 | admin: 46 | scopes: ["public.read", "public.write", "private.read", "private.write", "introspect", "offline"] 47 | grants: ["authorization_code", "implicit", "refresh_token", "client_credentials"] 48 | user: 49 | scopes: ["public.read", "public.write", "private.read", "private.write", "offline"] 50 | grants: ["authorization_code", "implicit", "refresh_token"] 51 | allowed-responses: ["code", "token", "id_token"] 52 | 53 | # Mailer configuration 54 | mailer: 55 | driver: mailgun 56 | options: 57 | domain: $MG_DOMAIN 58 | address: $MG_ADDRESS 59 | key: $MG_APIKEY 60 | secret: $MG_PRIKEY 61 | 62 | -------------------------------------------------------------------------------- /lib/api/messages.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * API messages and response types 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package api 8 | 9 | import () 10 | 11 | // Response Common API response object 12 | type Response struct { 13 | // Response code 14 | Code string `json:"code"` 15 | } 16 | 17 | // API Response Messages for frontend / internationalisation parsing 18 | const ( 19 | // General messages 20 | NotImplemented = "NotImplemented" 21 | InternalError = "InternalError" 22 | FormParsingError = "FormParsingError" 23 | DecodingFailed = "DecodingFailed" 24 | ActionMissing = "ActionMissing" 25 | IncorrectArguments = "IncorrectArguments" 26 | OK = "OK" 27 | 28 | // User input messages 29 | MissingEmail = "MissingEmail" 30 | InvalidEmail = "InvalidEmail" 31 | InvalidUsername = "InvalidUsername" 32 | MissingPassword = "MissingPassword" 33 | PasswordComplexityTooLow = "PasswordComplexityTooLow" 34 | DuplicateUserAccount = "DuplicateUserAccount" 35 | CreateUserSuccess = "CreateUserSuccess" 36 | 37 | // Status messages 38 | LoginSuccessful = "LoginSuccessful" 39 | LogoutSuccessful = "LogoutSuccessful" 40 | ActivationSuccessful = "ActivationSuccessful" 41 | AccountLocked = "AccountLocked" 42 | UnlockSuccessful = "UnlockSuccessful" 43 | PasswordUpdated = "PasswordUpdated" 44 | AlreadyAuthenticated = "AlreadyAuthenticated" 45 | Unauthorized = "Unauthorized" 46 | InvalidToken = "InvalidToken" 47 | MissingToken = "MissingToken" 48 | NoRecoveryPending = "NoRecoveryPending" 49 | LoginRequired = "LoginRequired" 50 | 51 | // Second factor messages 52 | SecondFactorRequired = "SecondFactorRequired" 53 | SecondFactorNoRequestSession = "SecondFactorNoSession" 54 | SecondFactorInvalidSession = "SecondFactorInvalidSession" 55 | SecondFactorBadResponse = "SecondFactorBadResponse" 56 | SecondFactorSuccess = "SecondFactorSuccess" 57 | SecondFactorFailed = "SecondFactorFailed" 58 | TokenNameRequired = "TokenNameRequired" 59 | SecondFactorNotFound = "SecondFactorNotFound" 60 | 61 | U2FRegistrationFailed = "U2FRegistrationFailed" 62 | U2FRegistrationComplete = "U2FRegistrationComplete" 63 | NoU2FPending = "NoU2FPending" 64 | NoU2FTokenFound = "NoU2FTokenFound" 65 | U2FTokenRemoved = "U2FTokenRemoved" 66 | RecoveryNoRequestPending = "RecoveryNoRequestPending" 67 | 68 | TOTPTokenRemoved = "TOTPTokenRemoved" 69 | 70 | BackupTokenOverwriteRequired = "CreateBackupTokenOverwriteRequired" 71 | BackupTokensRemoved = "BackupTokensRemoved" 72 | 73 | // OAuth messages 74 | OAuthInvalidClientName = "OAuthInvalidClientName" 75 | OAuthInvalidRedirect = "OAuthInvalidRedirect" 76 | OAuthNoAuthorizePending = "OAuthNoAuthorizePending" 77 | OAuthNoTokenFound = "OAuthNoTokenFound" 78 | OAuthNoGrantedScopes = "OAuthNoGrantedScopes" 79 | OAuthMissingAccessToken = "OAuthMissingAccessToken" 80 | ) 81 | -------------------------------------------------------------------------------- /lib/api/paginate.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Messages and types for pagination 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package api 8 | 9 | type Paginate struct { 10 | Count uint 11 | Offset uint 12 | } 13 | -------------------------------------------------------------------------------- /lib/api/token.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Messages and types for token handler implementations 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package api 8 | 9 | import ( 10 | "errors" 11 | ) 12 | 13 | // Token action type for interface 14 | type TokenAction string 15 | 16 | // Token success actions 17 | const TokenActionActivate TokenAction = "activate" 18 | const TokenActionUnlock TokenAction = "unlock" 19 | const TokenActionRecovery TokenAction = "recover" 20 | 21 | // Token error actions 22 | const TokenActionInvalid TokenAction = "invalid" 23 | const TokenActionExpired TokenAction = "expired" 24 | 25 | var TokenError = errors.New("internal server error") 26 | var TokenErrorInvalidUser = errors.New("action token invalid user") 27 | var TokenErrorInvalidAction = errors.New("action token invalid action") 28 | var TokenErrorAlreadyUsed = errors.New("action token already used") 29 | var TokenErrorNotFound = errors.New("action token not found") 30 | -------------------------------------------------------------------------------- /lib/appcontext/bind.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Application context session storage and binding 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package appcontext 8 | 9 | import ( 10 | "log" 11 | 12 | "github.com/gocraft/web" 13 | "github.com/gorilla/sessions" 14 | ) 15 | 16 | // BindInst Binds an object instance to a session key and writes to the browser session store 17 | // TODO: Bindings should probably time out eventually 18 | func (c *AuthPlzCtx) BindInst(rw web.ResponseWriter, req *web.Request, sessionKey, dataKey string, inst interface{}) error { 19 | session, err := c.Global.SessionStore.Get(req.Request, sessionKey) 20 | if err != nil { 21 | log.Printf("AuthPlzCtx.Bind error fetching session-key:%s (%s)", sessionKey, err) 22 | c.WriteInternalError(rw) 23 | return err 24 | } 25 | 26 | session.Values[dataKey] = inst 27 | session.Save(req.Request, rw) 28 | 29 | return nil 30 | } 31 | 32 | // GetInst Fetches an object instance by session key from the browser session store 33 | func (c *AuthPlzCtx) GetInst(rw web.ResponseWriter, req *web.Request, sessionKey, dataKey string) (interface{}, error) { 34 | session, err := c.Global.SessionStore.Get(req.Request, sessionKey) 35 | if err != nil { 36 | log.Printf("AuthPlzCtx.GetInst error fetching session-key:%s (%s)", sessionKey, err) 37 | c.WriteInternalError(rw) 38 | return nil, err 39 | } 40 | 41 | if session.Values[dataKey] == nil { 42 | log.Printf("AuthPlzCtx.GetInst error no dataKey: %s found in session: %s", dataKey, sessionKey) 43 | c.WriteInternalError(rw) 44 | return nil, err 45 | } 46 | 47 | return session.Values[dataKey], nil 48 | } 49 | 50 | // GetNamedSession fetches a session by name 51 | func (c *AuthPlzCtx) GetNamedSession(rw web.ResponseWriter, req *web.Request, sessionKey string) (*sessions.Session, error) { 52 | session, err := c.Global.SessionStore.Get(req.Request, sessionKey) 53 | if err != nil { 54 | log.Printf("AuthPlzCtx.GetInst error fetching session-key:%s (%s)", sessionKey, err) 55 | return nil, err 56 | } 57 | return session, nil 58 | } 59 | -------------------------------------------------------------------------------- /lib/appcontext/bind_2fa.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Context helpers for second factor authentication 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package appcontext 8 | 9 | import ( 10 | "log" 11 | "net/http" 12 | 13 | "github.com/authplz/authplz-core/lib/api" 14 | "github.com/gocraft/web" 15 | ) 16 | 17 | // SecondFactorRequest is a request for 2fa 18 | // This is used to register a request that can be fetched by 2fa implementations 19 | type SecondFactorRequest struct { 20 | UserID string 21 | Action string 22 | } 23 | 24 | const ( 25 | secondFactorRequestSessionKey = "2fa-request-session" 26 | secondFactorUserIDKey = "2fa-user-id" 27 | secondFactorActionKey = "2fa-action" 28 | 29 | secondFactorTimeout = 60 * 10 30 | ) 31 | 32 | // Bind2FARequest Bind a 2fa request and action for a user 33 | func (c *AuthPlzCtx) Bind2FARequest(rw web.ResponseWriter, req *web.Request, userID string, action string) { 34 | log.Printf("AuthPlzCtx.Bind2faRequest adding 2fa request session for user %s\n", userID) 35 | 36 | session, err := c.GetNamedSession(rw, req, secondFactorRequestSessionKey) 37 | if err != nil { 38 | c.WriteInternalError(rw) 39 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.SecondFactorNoRequestSession) 40 | return 41 | } 42 | 43 | session.Values[secondFactorUserIDKey] = userID 44 | session.Values[secondFactorActionKey] = action 45 | session.Options.MaxAge = secondFactorTimeout 46 | 47 | session.Save(req.Request, rw) 48 | } 49 | 50 | // Get2FARequest Fetch a 2fa request and action for a user 51 | func (c *AuthPlzCtx) Get2FARequest(rw web.ResponseWriter, req *web.Request) (string, string) { 52 | session, err := c.GetNamedSession(rw, req, secondFactorRequestSessionKey) 53 | if err != nil { 54 | return "", "" 55 | } 56 | 57 | userID, ok1 := session.Values[secondFactorUserIDKey] 58 | action, ok2 := session.Values[secondFactorActionKey] 59 | 60 | if !ok1 || !ok2 { 61 | return "", "" 62 | } 63 | 64 | session.Options.MaxAge = -1 65 | session.Save(req.Request, rw) 66 | 67 | return userID.(string), action.(string) 68 | } 69 | -------------------------------------------------------------------------------- /lib/appcontext/bind_flash.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Application context flash message handlers 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package appcontext 8 | 9 | import ( 10 | "github.com/gocraft/web" 11 | ) 12 | 13 | // Helper function to set a flash message for display to the user 14 | func (c *AuthPlzCtx) SetFlashMessage(message string, rw web.ResponseWriter, req *web.Request) { 15 | session, err := c.Global.SessionStore.Get(req.Request, "user-message") 16 | if err != nil { 17 | return 18 | } 19 | session.AddFlash(message) 20 | 21 | c.session.Save(req.Request, rw) 22 | } 23 | 24 | // Helper function to get a flash message to display to the user 25 | func (c *AuthPlzCtx) GetFlashMessage(rw web.ResponseWriter, req *web.Request) string { 26 | session, err := c.Global.SessionStore.Get(req.Request, "user-message") 27 | if err != nil { 28 | return "" 29 | } 30 | 31 | flashes := session.Flashes() 32 | if len(flashes) > 0 { 33 | return flashes[0].(string) 34 | } 35 | 36 | return "" 37 | } 38 | -------------------------------------------------------------------------------- /lib/appcontext/bind_recovery.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Application context handlers for recovery sessions 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package appcontext 8 | 9 | import ( 10 | "log" 11 | "net/http" 12 | 13 | "github.com/authplz/authplz-core/lib/api" 14 | "github.com/gocraft/web" 15 | ) 16 | 17 | const ( 18 | recoveryRequestSessionKey = "recovery-request-session" 19 | recoveryRequestUserIDKey = "recovery-request-userID" 20 | recoveryRequestExpiry = 60 * 10 21 | ) 22 | 23 | // BindRecoveryRequest binds an authenticated recovery request to the session 24 | // This should only be called after all [possible] authentication has been executed 25 | func (c *AuthPlzCtx) BindRecoveryRequest(userID string, rw web.ResponseWriter, req *web.Request) { 26 | log.Printf("AuthPlzCtx.BindRecoveryRequest adding recovery request session for user %s\n", userID) 27 | 28 | session, err := c.GetNamedSession(rw, req, recoveryRequestSessionKey) 29 | if err != nil { 30 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.RecoveryNoRequestPending) 31 | return 32 | } 33 | 34 | session.Values[recoveryRequestUserIDKey] = userID 35 | session.Options.MaxAge = recoveryRequestExpiry 36 | session.Save(req.Request, rw) 37 | } 38 | 39 | // GetRecoveryRequest fetches an authenticated recovery request from the session 40 | // This allows a module to accept new password settings for the provided user id 41 | func (c *AuthPlzCtx) GetRecoveryRequest(rw web.ResponseWriter, req *web.Request) string { 42 | session, err := c.GetNamedSession(rw, req, recoveryRequestSessionKey) 43 | if err != nil { 44 | log.Printf("AuthPlzCtx.GetRecoveryRequest No recovery request session found") 45 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.RecoveryNoRequestPending) 46 | return "" 47 | } 48 | 49 | userID := session.Values[recoveryRequestUserIDKey] 50 | if userID == nil { 51 | log.Printf("AuthPlzCtx.GetRecoveryRequest No recovery request session found") 52 | return "" 53 | } 54 | 55 | session.Options.MaxAge = -1 56 | session.Save(req.Request, rw) 57 | 58 | return userID.(string) 59 | } 60 | -------------------------------------------------------------------------------- /lib/appcontext/context.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Core application context 3 | * This base context is available on all endpoints 4 | * 5 | * Copyright 2018 Ryan Kurte 6 | */ 7 | 8 | package appcontext 9 | 10 | import ( 11 | "encoding/gob" 12 | "log" 13 | "net" 14 | "net/http" 15 | "time" 16 | 17 | "github.com/gocraft/web" 18 | "github.com/gorilla/sessions" 19 | //"github.com/authplz/authplz-core/lib/api" 20 | ) 21 | 22 | func init() { 23 | gob.Register(SudoSession{}) 24 | gob.Register(SecondFactorRequest{}) 25 | } 26 | 27 | // AuthPlzGlobalCtx Application global / static context 28 | type AuthPlzGlobalCtx struct { 29 | SessionStore *sessions.CookieStore 30 | } 31 | 32 | // NewGlobalCtx creates a new global context instance 33 | func NewGlobalCtx(sessionStore *sessions.CookieStore) AuthPlzGlobalCtx { 34 | return AuthPlzGlobalCtx{sessionStore} 35 | } 36 | 37 | // AuthPlzCtx is the common per-request context 38 | // Modules implement their own contexts that extend this as a base 39 | type AuthPlzCtx struct { 40 | Global *AuthPlzGlobalCtx 41 | session *sessions.Session 42 | userid string 43 | message string 44 | remoteAddr string 45 | forwardedFor string 46 | locale string 47 | meta map[string]string 48 | } 49 | 50 | // User is the user instance interface used in the app context 51 | type User interface { 52 | GetExtID() string 53 | IsAdmin() string 54 | } 55 | 56 | // MiddlewareFunc Convenience type to describe middleware functions 57 | type MiddlewareFunc func(c *AuthPlzCtx, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) 58 | 59 | // BindContext Helper to bind the global context object into the router context 60 | // This is a closure to run over an instance of the global context 61 | func BindContext(globalCtx *AuthPlzGlobalCtx) MiddlewareFunc { 62 | return func(ctx *AuthPlzCtx, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 63 | if ctx.meta == nil { 64 | ctx.meta = make(map[string]string) 65 | } 66 | ctx.Global = globalCtx 67 | next(rw, req) 68 | } 69 | } 70 | 71 | // GetSession fetches the base user session instance 72 | // Modules can use this base session or their own session instances 73 | func (c *AuthPlzCtx) GetSession() *sessions.Session { 74 | return c.session 75 | } 76 | 77 | // SessionMiddleware User session layer 78 | // Middleware matches user session if it exists and saves userid to the session object 79 | func (c *AuthPlzCtx) SessionMiddleware(rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 80 | session, _ := c.Global.SessionStore.Get(req.Request, "user-session") 81 | 82 | // Save session for further use 83 | c.session = session 84 | 85 | // TODO: load user from session 86 | 87 | session.Save(req.Request, rw) 88 | next(rw, req) 89 | } 90 | 91 | // GetIPMiddleware Middleware to grab IP & forwarding headers and store in session 92 | func (c *AuthPlzCtx) GetIPMiddleware(rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 93 | c.meta["remote-address"], _, _ = net.SplitHostPort(req.RemoteAddr) 94 | c.meta["forwarded-for"] = req.Header.Get("x-forwarded-for") 95 | 96 | next(rw, req) 97 | } 98 | 99 | // RequireAccountMiddleware to ensure only logged in access to an endpoint 100 | func (c *AuthPlzCtx) RequireAccountMiddleware(rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 101 | if c.userid == "" { 102 | c.WriteUnauthorized(rw) 103 | } else { 104 | next(rw, req) 105 | } 106 | } 107 | 108 | // LoginUser Helper function to login a user 109 | func (c *AuthPlzCtx) LoginUser(userid string, rw web.ResponseWriter, req *web.Request) { 110 | if c.session == nil { 111 | log.Printf("Error logging in user, no session found") 112 | return 113 | } 114 | 115 | c.session.Values["userId"] = userid 116 | c.session.Save(req.Request, rw) 117 | c.userid = userid 118 | log.Printf("Context: logged in user %s", userid) 119 | } 120 | 121 | // LogoutUser Helper function to logout a user 122 | func (c *AuthPlzCtx) LogoutUser(rw web.ResponseWriter, req *web.Request) { 123 | log.Printf("Context: logging out user %s", c.userid) 124 | c.session.Options.MaxAge = -1 125 | c.session.Save(req.Request, rw) 126 | c.userid = "" 127 | } 128 | 129 | func (c *AuthPlzCtx) GetMeta() map[string]string { 130 | return c.meta 131 | } 132 | 133 | // GetUserID Fetch user id from a session 134 | // Blank if a user is not logged in 135 | func (c *AuthPlzCtx) GetUserID() string { 136 | id := c.session.Values["userId"] 137 | if id != nil { 138 | return id.(string) 139 | } else { 140 | return "" 141 | } 142 | } 143 | 144 | // UserAction executes a user action, such as `login` 145 | // This is provided to allow modules to execute global actions as a given user across the API boundaries 146 | // For example, this allows 2fa to be used to validate a user action 147 | // TODO: a more elegant solution to this could be nice. 148 | func (c *AuthPlzCtx) UserAction(userid, action string, rw web.ResponseWriter, req *web.Request) { 149 | switch action { 150 | case "login": 151 | c.LoginUser(userid, rw, req) 152 | case "recover": 153 | c.BindRecoveryRequest(userid, rw, req) 154 | case "sudo": 155 | // TODO: how to propagate duration through to here? 156 | c.SetSudo(userid, time.Minute*5, rw, req) 157 | default: 158 | log.Printf("AuthPlzCtx.UserAction error: unrecognised user action (%s)", action) 159 | } 160 | } 161 | 162 | const ( 163 | redirectSessionKey = "redirect-session" 164 | redirectURLKey = "redirect-url" 165 | ) 166 | 167 | // DoRedirect writes a redirect to the client 168 | func (c *AuthPlzCtx) DoRedirect(url string, rw web.ResponseWriter, req *web.Request) { 169 | http.Redirect(rw, req.Request, url, http.StatusFound) 170 | } 171 | 172 | // BindRedirect binds a redirect URL to the user session 173 | // This is called post-login (or other action) to allow users to return to 174 | func (c *AuthPlzCtx) BindRedirect(url string, rw web.ResponseWriter, req *web.Request) { 175 | c.BindInst(rw, req, redirectSessionKey, redirectURLKey, url) 176 | } 177 | 178 | // GetRedirect fetches a redirect from a user session to allow for 179 | // post-login (or re-auth) user redirection 180 | func (c *AuthPlzCtx) GetRedirect(rw web.ResponseWriter, req *web.Request) string { 181 | url, err := c.GetInst(rw, req, redirectSessionKey, redirectURLKey) 182 | if err != nil { 183 | return "" 184 | } 185 | 186 | urlStr, ok := url.(string) 187 | if !ok { 188 | return "" 189 | } 190 | 191 | return urlStr 192 | } 193 | -------------------------------------------------------------------------------- /lib/appcontext/helpers.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Application context helpers 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package appcontext 8 | 9 | import ( 10 | "encoding/json" 11 | "github.com/authplz/authplz-core/lib/api" 12 | "log" 13 | "net/http" 14 | ) 15 | 16 | // WriteJSON Helper to write objects out as JSON 17 | func (c *AuthPlzCtx) WriteJSON(w http.ResponseWriter, i interface{}) { 18 | c.WriteJSONWithStatus(w, http.StatusOK, i) 19 | } 20 | 21 | // WriteJSON Helper to write objects out as JSON 22 | func (c *AuthPlzCtx) WriteJSONWithStatus(w http.ResponseWriter, status int, i interface{}) { 23 | js, err := json.Marshal(i) 24 | if err != nil { 25 | log.Print(err) 26 | w.WriteHeader(http.StatusInternalServerError) 27 | return 28 | } 29 | 30 | w.Header().Set("Content-Type", "application/json") 31 | w.WriteHeader(status) 32 | w.Write(js) 33 | } 34 | 35 | // WriteAPIResult Helper to write API result messages 36 | func (c *AuthPlzCtx) WriteAPIResult(w http.ResponseWriter, code string) { 37 | apiResp := api.Response{Code: code} 38 | c.WriteJSON(w, apiResp) 39 | } 40 | 41 | // WriteAPIResultWithCode Helper to write API result messsages while setting the HTTP response code 42 | func (c *AuthPlzCtx) WriteAPIResultWithCode(w http.ResponseWriter, status int, code string) { 43 | apiResp := api.Response{Code: code} 44 | 45 | js, err := json.Marshal(&apiResp) 46 | if err != nil { 47 | log.Print(err) 48 | w.WriteHeader(http.StatusInternalServerError) 49 | return 50 | } 51 | 52 | w.Header().Set("Content-Type", "application/json") 53 | w.Header().Set("message", string(code)) 54 | w.WriteHeader(status) 55 | 56 | w.Write(js) 57 | } 58 | 59 | // WriteUnauthorized helper to write unauthorized status and message 60 | func (c *AuthPlzCtx) WriteUnauthorized(w http.ResponseWriter) { 61 | c.WriteAPIResultWithCode(w, http.StatusUnauthorized, api.Unauthorized) 62 | } 63 | 64 | // WriteInternalError helper to write internal error status and message 65 | func (c *AuthPlzCtx) WriteInternalError(w http.ResponseWriter) { 66 | c.WriteAPIResultWithCode(w, http.StatusInternalServerError, api.InternalError) 67 | } 68 | -------------------------------------------------------------------------------- /lib/appcontext/sudo.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Application context "sudo" implementation (WIP) 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package appcontext 8 | 9 | import ( 10 | "log" 11 | "time" 12 | 13 | "github.com/gocraft/web" 14 | ) 15 | 16 | const ( 17 | // SudoSessionKey is the cookie key used for sudo session storage 18 | sudoSessionKey = "sudo-session" 19 | // Timeout for sudo sessions 20 | sudoTimeout = 60 * 10 // 10 minutes 21 | ) 22 | 23 | // SudoSession used to store user reauthorization sessions for protected account actions 24 | // Such as password changes or 2fa alterations 25 | type SudoSession struct { 26 | UserID string 27 | SessionStart time.Time 28 | SessionEnd time.Time 29 | } 30 | 31 | // SetSudo used to indicate a user has reauthorized to allow protected account actions 32 | // TODO: could this be pinned to more things? (user agent, IP, real invalidation so it can't be reused if cancelled?) 33 | // Guess re-use is a bit moot given there is no reason to cancel atm 34 | func (c *AuthPlzCtx) SetSudo(userID string, timeout time.Duration, rw web.ResponseWriter, req *web.Request) { 35 | log.Printf("AuthPlzCtx.SetSudo: creating sudo session fo user %s", c.userid) 36 | 37 | session, err := c.GetNamedSession(rw, req, sudoSessionKey) 38 | if err != nil { 39 | c.WriteInternalError(rw) 40 | return 41 | } 42 | 43 | sudoSession := SudoSession{ 44 | UserID: userID, 45 | SessionStart: time.Now(), 46 | SessionEnd: time.Now().Add(timeout), 47 | } 48 | 49 | session.Values[sudoSessionKey] = sudoSession 50 | session.Options.MaxAge = sudoTimeout 51 | session.Save(req.Request, rw) 52 | } 53 | 54 | // ClearSudo removes a sudo session from a user session 55 | func (c *AuthPlzCtx) ClearSudo(rw web.ResponseWriter, req *web.Request) { 56 | log.Printf("AuthPlzCtx.ClearSudo: ending sudo session fo user %s", c.userid) 57 | 58 | session, err := c.GetNamedSession(rw, req, sudoSessionKey) 59 | if err != nil { 60 | c.WriteInternalError(rw) 61 | return 62 | } 63 | 64 | session.Options.MaxAge = -1 65 | session.Save(req.Request, rw) 66 | } 67 | 68 | // CanSudo checks whether a user has a current sudo session 69 | func (c *AuthPlzCtx) CanSudo(rw web.ResponseWriter, req *web.Request) bool { 70 | session, err := c.GetNamedSession(rw, req, sudoSessionKey) 71 | if err != nil { 72 | c.WriteInternalError(rw) 73 | return false 74 | } 75 | s := session.Values[sudoSessionKey] 76 | if s == nil { 77 | return false 78 | } 79 | sudoSession, ok := s.(SudoSession) 80 | if !ok { 81 | c.ClearSudo(rw, req) 82 | return false 83 | } 84 | if time.Now().Before(sudoSession.SessionStart) { 85 | c.ClearSudo(rw, req) 86 | return false 87 | } 88 | if time.Now().After(sudoSession.SessionEnd) { 89 | c.ClearSudo(rw, req) 90 | return false 91 | } 92 | if sudoSession.UserID != c.GetUserID() { 93 | c.ClearSudo(rw, req) 94 | return false 95 | } 96 | return true 97 | } 98 | -------------------------------------------------------------------------------- /lib/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * AuthPlz Application Configuration 3 | * Defines configuration arguments and environmental variables 4 | * 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package config 9 | 10 | import ( 11 | "crypto/rand" 12 | "encoding/base64" 13 | "errors" 14 | "fmt" 15 | "log" 16 | 17 | "io/ioutil" 18 | 19 | "github.com/jessevdk/go-flags" 20 | "github.com/ryankurte/go-structparse" 21 | "gopkg.in/yaml.v2" 22 | ) 23 | 24 | // CLIOptions defines options that can be passed on the command line 25 | // other options must be passed through the configuration file 26 | type CLIOptions struct { 27 | ConfigFile string `short:"c" long:"config" description:"AuthPlz configuration file" default:"./authplz.yml"` 28 | Prefix string `short:"p" long:"prefix" description:"Prefix for environmental variable loading" default:"AUTHPLZ_"` 29 | } 30 | 31 | // AuthPlzConfig configuration structure 32 | type AuthPlzConfig struct { 33 | Name string `yaml:"name"` 34 | Address string `yaml:"bind-address"` 35 | Port string `yaml:"bind-port"` 36 | ExternalAddress string `yaml:"external-address"` 37 | AllowedOrigins []string `yaml:"allowed-origins"` 38 | DisableWebSecurity bool `yaml:"disable-web-security"` 39 | 40 | Database string `yaml:"database"` 41 | CookieSecret string `yaml:"cookie-secret"` 42 | TokenSecret string `yaml:"token-secret"` 43 | 44 | StaticDir string `yaml:"static-dir"` 45 | TemplateDir string `yaml:"template-dir"` 46 | 47 | TLS TLSConfig `yaml:"tls"` 48 | OAuth OAuthConfig `yaml:"oauth"` 49 | Mailer MailerConfig `yaml:"mailer"` 50 | 51 | MinimumPasswordLength int `yaml:"password-len"` 52 | } 53 | 54 | // GenerateSecret Helper to generate a default secret to use 55 | func GenerateSecret(len int) (string, error) { 56 | data := make([]byte, len) 57 | n, err := rand.Read(data) 58 | if err != nil { 59 | return "", err 60 | } 61 | if n != len { 62 | return "", errors.New("Config: RNG failed") 63 | } 64 | 65 | return base64.URLEncoding.EncodeToString(data), nil 66 | } 67 | 68 | // DefaultConfig Generate default configuration 69 | func DefaultConfig() (*AuthPlzConfig, error) { 70 | var c AuthPlzConfig 71 | var err error 72 | 73 | c.Name = "AuthPlz" 74 | c.Address = "localhost" 75 | c.Port = "9000" 76 | c.Database = "host=localhost user=postgres dbname=postgres sslmode=disable password=postgres" 77 | 78 | // Certificate files in environment 79 | c.TLS.Cert = "server.pem" 80 | c.TLS.Key = "server.key" 81 | c.TLS.Disabled = false 82 | 83 | c.StaticDir = "./authplz-ui/build" 84 | c.TemplateDir = "./templates" 85 | 86 | c.MinimumPasswordLength = 12 87 | 88 | c.Mailer.Driver = "logger" 89 | c.Mailer.Options = make(map[string]string) 90 | 91 | c.OAuth = DefaultOAuthConfig() 92 | 93 | c.CookieSecret, err = GenerateSecret(64) 94 | if err != nil { 95 | return nil, err 96 | } 97 | c.TokenSecret, err = GenerateSecret(64) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | return &c, err 103 | } 104 | 105 | // LoadConfig loads configuration from the specified file, using the provided prefix for environmental vars 106 | func LoadConfig(filename, envPrefix string) (*AuthPlzConfig, error) { 107 | 108 | c, err := DefaultConfig() 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | // Load configuration file 114 | data, err := ioutil.ReadFile(filename) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | // Parse configuration file 120 | err = yaml.Unmarshal(data, c) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | // Load specified variables from the environment 126 | em := structparse.NewEnvironmentMapper("$", envPrefix) 127 | structparse.Strings(em, c) 128 | 129 | // Load external address if not specified 130 | if c.ExternalAddress == "" { 131 | prefix := "https" 132 | if c.TLS.Disabled { 133 | prefix = "http" 134 | } 135 | c.ExternalAddress = fmt.Sprintf("%s://%s:%s", prefix, c.Address, c.Port) 136 | } 137 | 138 | // Populate allowed origins with external address if unspecified 139 | if len(c.AllowedOrigins) == 0 { 140 | c.AllowedOrigins = []string{c.ExternalAddress} 141 | } 142 | 143 | // Decode secrets to strings 144 | tokenSecret, err := base64.URLEncoding.DecodeString(c.TokenSecret) 145 | if err != nil { 146 | log.Panicf("Error decoding token secret: %s", err) 147 | } 148 | 149 | cookieSecret, err := base64.URLEncoding.DecodeString(c.CookieSecret) 150 | if err != nil { 151 | log.Panicf("Error decoding cookie secret: %s", err) 152 | } 153 | 154 | oauthSecret, err := base64.URLEncoding.DecodeString(c.OAuth.TokenSecret) 155 | if err != nil { 156 | log.Panicf("Error decoding oauth secret: %s", err) 157 | } 158 | 159 | c.TokenSecret = string(tokenSecret) 160 | c.CookieSecret = string(cookieSecret) 161 | c.OAuth.TokenSecret = string(oauthSecret) 162 | 163 | return c, nil 164 | } 165 | 166 | // GetConfig fetches the server configuration 167 | // This parses environmental variables, command line flags, and handles file based loading of configurations. 168 | func GetConfig() (*AuthPlzConfig, error) { 169 | 170 | // Load command line arguments 171 | cli := CLIOptions{} 172 | _, err := flags.Parse(&cli) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | return LoadConfig(cli.ConfigFile, cli.Prefix) 178 | } 179 | -------------------------------------------------------------------------------- /lib/config/config_test.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Configuration tests 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | package config 7 | 8 | import ( 9 | "testing" 10 | 11 | "encoding/base64" 12 | "github.com/stretchr/testify/assert" 13 | "os" 14 | ) 15 | 16 | func TestConfig(t *testing.T) { 17 | 18 | testCookieSecret := "TEST_COOKIE_SECRET" 19 | os.Setenv("AUTHPLZ_COOKIE_SECRET", base64.URLEncoding.EncodeToString([]byte(testCookieSecret))) 20 | testTokenSecret := "TEST_TOKEN_SECRET" 21 | os.Setenv("AUTHPLZ_TOKEN_SECRET", base64.URLEncoding.EncodeToString([]byte(testTokenSecret))) 22 | 23 | // GetConfig should load defaults, example config, and infil vars (if set) 24 | c, err := LoadConfig("../../authplz.yml", "AUTHPLZ_") 25 | assert.Nil(t, err) 26 | 27 | assert.EqualValues(t, "AuthPlz Example", c.Name) 28 | assert.EqualValues(t, "0.0.0.0", c.Address) 29 | assert.EqualValues(t, "9000", c.Port) 30 | //assert.EqualValues(t, "https://localhost:3000", c.ExternalAddress) 31 | 32 | assert.EqualValues(t, testCookieSecret, c.CookieSecret) 33 | assert.EqualValues(t, testTokenSecret, c.TokenSecret) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /lib/config/mailer.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Mailer configuration 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package config 8 | 9 | // MailerConfig Mailer configuration options 10 | type MailerConfig struct { 11 | Driver string `yaml:"driver"` 12 | Options map[string]string `yaml:"options"` 13 | } 14 | -------------------------------------------------------------------------------- /lib/config/oauth.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * OAuth configuration 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package config 8 | 9 | import ( 10 | "time" 11 | ) 12 | 13 | type configSplit struct { 14 | Admin []string 15 | User []string 16 | } 17 | 18 | // OAuthConfig OAuth controller configuration structure 19 | type OAuthConfig struct { 20 | // Redirect to client app for oauth authorization 21 | AuthorizeRedirect string 22 | // Secret for OAuth token attestation 23 | TokenSecret string 24 | // AllowedScopes defines the scopes a client can grant for admins and users 25 | AllowedScopes configSplit 26 | // AllowedGrants defines the grant types a client can support for admins and users 27 | AllowedGrants configSplit 28 | // AllowedResponses defines response types a client can support 29 | AllowedResponses []string 30 | // AccessExpiry is Access Token expiry time 31 | AccessExpiry time.Duration 32 | // IDExpiry is ID Token expiry time 33 | IDExpiry time.Duration 34 | // AuthorizeExpiry is Authorization token expiry time 35 | AuthorizeExpiry time.Duration 36 | // RefreshExpiry is Refresh token expiry time 37 | RefreshExpiry time.Duration 38 | } 39 | 40 | // DefaultOAuthConfig generates a default configuration for the OAuth module 41 | func DefaultOAuthConfig() OAuthConfig { 42 | secret, _ := GenerateSecret(64) 43 | return OAuthConfig{ 44 | AuthorizeRedirect: "/#/oauth-authorize", 45 | TokenSecret: secret, 46 | AllowedScopes: configSplit{ 47 | Admin: []string{"public.read", "public.write", "private.read", "private.write", "introspect", "offline"}, 48 | User: []string{"public.read", "public.write", "private.read", "private.write", "offline"}, 49 | }, 50 | AllowedGrants: configSplit{ 51 | Admin: []string{"authorization_code", "implicit", "refresh_token", "client_credentials"}, 52 | User: []string{"authorization_code", "implicit", "refresh_token"}, 53 | }, 54 | AllowedResponses: []string{"code", "token", "id_token"}, 55 | AccessExpiry: time.Hour * 24 * 1, 56 | IDExpiry: time.Hour * 24 * 1, 57 | AuthorizeExpiry: time.Hour * 24 * 1, 58 | RefreshExpiry: time.Hour * 24 * 180, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/config/tls.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * TLS Configuration 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package config 8 | 9 | // TLSConfig TLS configuration options 10 | type TLSConfig struct { 11 | Cert string `yaml:"cert"` 12 | Key string `yaml:"key"` 13 | Disabled bool `yaml:"disabled"` 14 | } 15 | -------------------------------------------------------------------------------- /lib/controllers/datastore/actiontoken.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Datastore - Action Tokens 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package datastore 8 | 9 | import "time" 10 | 11 | import "github.com/jinzhu/gorm" 12 | import "fmt" 13 | 14 | // ActionToken Time based One Time Password Token object 15 | type ActionToken struct { 16 | gorm.Model 17 | TokenID string 18 | UserExtID string 19 | UserID uint 20 | Action string 21 | Used bool 22 | UsedAt time.Time 23 | ExpiresAt time.Time 24 | } 25 | 26 | // Getters and setters for external interface compliance 27 | 28 | // GetTokenID fetches the action token ID 29 | func (token *ActionToken) GetTokenID() string { return token.TokenID } 30 | 31 | func (token *ActionToken) GetUserExtID() string { return token.UserExtID } 32 | 33 | // GetAction fetches the token action 34 | func (token *ActionToken) GetAction() string { return token.Action } 35 | 36 | // GetExpiry fetches the token expiry time 37 | func (token *ActionToken) GetExpiry() time.Time { return token.ExpiresAt } 38 | 39 | // IsUsed checks if a token has been used 40 | func (token *ActionToken) IsUsed() bool { return token.Used } 41 | 42 | // SetUsed sets the used state for the action token 43 | func (token *ActionToken) SetUsed(t time.Time) { 44 | token.Used = true 45 | token.UsedAt = t 46 | } 47 | 48 | // CreateActionToken adds an action token to the provided user account 49 | func (ds *DataStore) CreateActionToken(userExtID, tokenID, action string, expiry time.Time) (interface{}, error) { 50 | // Fetch user 51 | u, err := ds.GetUserByExtID(userExtID) 52 | if err != nil { 53 | return nil, err 54 | } 55 | if u == nil { 56 | return nil, fmt.Errorf("No user found by ID: %s", userExtID) 57 | } 58 | 59 | user := u.(*User) 60 | ActionToken := ActionToken{ 61 | UserID: user.ID, 62 | UserExtID: userExtID, 63 | TokenID: tokenID, 64 | Action: action, 65 | Used: false, 66 | ExpiresAt: expiry, 67 | } 68 | 69 | user.ActionTokens = append(user.ActionTokens, ActionToken) 70 | _, err = ds.UpdateUser(user) 71 | return &ActionToken, err 72 | } 73 | 74 | // GetActionToken fetches an action token by token id 75 | func (ds *DataStore) GetActionToken(tokenID string) (interface{}, error) { 76 | var actionToken ActionToken 77 | 78 | // Grab tokens 79 | err := ds.db.Where(&ActionToken{TokenID: tokenID}).First(&actionToken).Error 80 | 81 | return &actionToken, err 82 | } 83 | 84 | // GetActionTokens fetches tokens attached to a given user 85 | func (ds *DataStore) GetActionTokens(userid string) ([]interface{}, error) { 86 | var ActionTokens []ActionToken 87 | 88 | // Fetch user 89 | u, err := ds.GetUserByExtID(userid) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | user := u.(*User) 95 | 96 | // Grab tokens 97 | err = ds.db.Model(user).Related(&ActionTokens).Error 98 | 99 | interfaces := make([]interface{}, len(ActionTokens)) 100 | for i, t := range ActionTokens { 101 | interfaces[i] = &t 102 | } 103 | 104 | return interfaces, err 105 | } 106 | 107 | // UpdateActionToken updates a TOTP token instance in the database 108 | func (ds *DataStore) UpdateActionToken(token interface{}) (interface{}, error) { 109 | 110 | err := ds.db.Save(token).Error 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | return token, nil 116 | } 117 | -------------------------------------------------------------------------------- /lib/controllers/datastore/auditevent.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Datastore - Audit event logging 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package datastore 8 | 9 | import ( 10 | "encoding/json" 11 | "github.com/jinzhu/gorm" 12 | "time" 13 | ) 14 | 15 | // AuditEvent for a user account 16 | type AuditEvent struct { 17 | gorm.Model 18 | UserID uint 19 | Type string 20 | Time time.Time 21 | Data string 22 | } 23 | 24 | // GetType fetches the type of the event 25 | func (ae *AuditEvent) GetType() string { return ae.Type } 26 | 27 | // GetTime fetches the time at which the event occured 28 | func (ae *AuditEvent) GetTime() time.Time { return ae.Time } 29 | 30 | // GetData fetches a map of the associated data 31 | func (ae *AuditEvent) GetData() (map[string]string, error) { 32 | data := make(map[string]string) 33 | 34 | err := json.Unmarshal([]byte(ae.Data), &data) 35 | 36 | return data, err 37 | } 38 | 39 | // AddAuditEvent creates an audit event in the database 40 | func (dataStore *DataStore) AddAuditEvent(userid, eventType string, eventTime time.Time, data map[string]string) (interface{}, error) { 41 | 42 | u, err := dataStore.GetUserByExtID(userid) 43 | if err != nil { 44 | return nil, err 45 | } 46 | user := u.(*User) 47 | 48 | encodedData, err := json.Marshal(data) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | auditEvent := AuditEvent{ 54 | UserID: user.ID, 55 | Type: eventType, 56 | Time: eventTime, 57 | Data: string(encodedData), 58 | } 59 | 60 | user.AuditEvents = append(user.AuditEvents, auditEvent) 61 | _, err = dataStore.UpdateUser(user) 62 | return user.AuditEvents, err 63 | } 64 | 65 | // GetAuditEvents fetches a list of audit events for a given userr 66 | func (dataStore *DataStore) GetAuditEvents(userid string) ([]interface{}, error) { 67 | var auditEvents []AuditEvent 68 | 69 | // Fetch user 70 | u, err := dataStore.GetUserByExtID(userid) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | user := u.(*User) 76 | 77 | err = dataStore.db.Model(user).Related(&auditEvents).Error 78 | 79 | interfaces := make([]interface{}, len(auditEvents)) 80 | for i, t := range auditEvents { 81 | interfaces[i] = &t 82 | } 83 | 84 | return interfaces, err 85 | } 86 | -------------------------------------------------------------------------------- /lib/controllers/datastore/backuptoken.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Datastore - 2fa backup tokens 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package datastore 8 | 9 | import ( 10 | "fmt" 11 | "time" 12 | ) 13 | 14 | import "github.com/jinzhu/gorm" 15 | 16 | // BackupToken 2fa backup code object 17 | type BackupToken struct { 18 | gorm.Model 19 | UserID uint 20 | Name string 21 | Secret string 22 | Used bool 23 | UsedAt time.Time 24 | } 25 | 26 | // Getters and setters for external interface compliance 27 | 28 | // GetName fetches the token Name 29 | func (token *BackupToken) GetName() string { return token.Name } 30 | 31 | // GetHashedSecret fetches the hashed token secret 32 | func (token *BackupToken) GetHashedSecret() string { return token.Secret } 33 | 34 | // IsUsed checks if a token has been used 35 | func (token *BackupToken) IsUsed() bool { return token.Used } 36 | 37 | // SetUsed marks a token as used 38 | func (token *BackupToken) SetUsed() { token.Used = true } 39 | 40 | func (token *BackupToken) GetUsedAt() time.Time { return token.UsedAt } 41 | 42 | func (token *BackupToken) GetCreatedAt() time.Time { return token.CreatedAt } 43 | 44 | // AddBackupToken creates a backupt token token instance to a user in the database 45 | func (dataStore *DataStore) AddBackupToken(userid, name, secret string) (interface{}, error) { 46 | 47 | // Fetch user 48 | u, err := dataStore.GetUserByExtID(userid) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | user := u.(*User) 54 | 55 | // Create a token instance 56 | token := BackupToken{ 57 | UserID: user.ID, 58 | Name: name, 59 | Secret: secret, 60 | Used: false, 61 | } 62 | 63 | // Add the token to the user and save 64 | user.BackupTokens = append(user.BackupTokens, token) 65 | _, err = dataStore.UpdateUser(user) 66 | return user, err 67 | } 68 | 69 | // AddBackupToken creates a backupt token token instance to a user in the database 70 | func (dataStore *DataStore) AddBackupTokens(userid string, names, secrets []string) (interface{}, error) { 71 | 72 | // Fetch user 73 | u, err := dataStore.GetUserByExtID(userid) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | user := u.(*User) 79 | 80 | if len(names) != len(secrets) { 81 | return nil, fmt.Errorf("Error: name and secret arrays must have matching lengths") 82 | } 83 | 84 | for i := range names { 85 | // Create a token instance 86 | token := BackupToken{ 87 | UserID: user.ID, 88 | Name: names[i], 89 | Secret: secrets[i], 90 | Used: false, 91 | } 92 | // Add the token to the user and save 93 | user.BackupTokens = append(user.BackupTokens, token) 94 | } 95 | 96 | // Update user instance 97 | _, err = dataStore.UpdateUser(user) 98 | return user, err 99 | } 100 | 101 | // GetBackupTokens fetches the backup tokens for the specified user 102 | func (dataStore *DataStore) GetBackupTokens(userid string) ([]interface{}, error) { 103 | var BackupTokens []BackupToken 104 | 105 | // Fetch user 106 | u, err := dataStore.GetUserByExtID(userid) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | user := u.(*User) 112 | 113 | err = dataStore.db.Model(user).Related(&BackupTokens).Error 114 | 115 | interfaces := make([]interface{}, len(BackupTokens)) 116 | for i, t := range BackupTokens { 117 | interfaces[i] = &t 118 | } 119 | 120 | return interfaces, err 121 | } 122 | 123 | // GetBackupTokenByName fetches the named backup token for a specified user 124 | func (dataStore *DataStore) GetBackupTokenByName(userid, name string) (interface{}, error) { 125 | var backupToken BackupToken 126 | 127 | // Fetch user 128 | u, err := dataStore.GetUserByExtID(userid) 129 | if err != nil { 130 | return nil, err 131 | } 132 | user := u.(*User) 133 | 134 | // Fetch backup token 135 | err = dataStore.db.Find(&BackupToken{UserID: user.ID, Name: name}).First(&backupToken).Error 136 | 137 | return &backupToken, err 138 | } 139 | 140 | // UpdateBackupToken updates a backup token instance 141 | func (dataStore *DataStore) UpdateBackupToken(token interface{}) (interface{}, error) { 142 | err := dataStore.db.Save(token).Error 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | return token, nil 148 | } 149 | 150 | // ClearPendingBackupTokens removes any unused backup tokens 151 | func (dataStore *DataStore) ClearPendingBackupTokens(userid string) error { 152 | u, err := dataStore.GetUserByExtID(userid) 153 | if err != nil { 154 | return err 155 | } 156 | user := u.(*User) 157 | 158 | err = dataStore.db.Delete(&BackupToken{UserID: user.ID, Used: false}).Error 159 | return err 160 | } 161 | -------------------------------------------------------------------------------- /lib/controllers/datastore/datastore.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Datastore - core 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package datastore 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | 13 | "github.com/jinzhu/gorm" 14 | _ "github.com/jinzhu/gorm/dialects/postgres" 15 | 16 | "github.com/authplz/authplz-core/lib/controllers/datastore/oauth2" 17 | ) 18 | 19 | var ErrUserNotFound = errors.New("User account not found") 20 | 21 | // DataStore instance storage 22 | type DataStore struct { 23 | db *gorm.DB 24 | *oauthstore.OauthStore 25 | } 26 | 27 | // QueryFilter filter types 28 | type QueryFilter struct { 29 | Limit uint // Number of objects to return 30 | Offset uint // Offset of objects to return 31 | } 32 | 33 | // NewDataStore Create a datastore instance 34 | func NewDataStore(dbString string) (*DataStore, error) { 35 | // Attempt database connection 36 | db, err := gorm.Open("postgres", dbString) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to connect database: %s", dbString) 39 | } 40 | 41 | //db = db.LogMode(true) 42 | 43 | ds := &DataStore{db: db} 44 | 45 | ds.OauthStore = oauthstore.NewOauthStore(db, ds) 46 | 47 | ds.Sync() 48 | 49 | return ds, nil 50 | } 51 | 52 | // Close an open datastore instance 53 | func (dataStore *DataStore) Close() { 54 | dataStore.db.Close() 55 | } 56 | 57 | func (dataStore *DataStore) Drop() { 58 | db := dataStore.db 59 | 60 | db = db.Exec("DROP TABLE IF EXISTS fido_tokens CASCADE;") 61 | db = db.Exec("DROP TABLE IF EXISTS totp_tokens CASCADE;") 62 | db = db.Exec("DROP TABLE IF EXISTS backup_tokens CASCADE;") 63 | db = db.Exec("DROP TABLE IF EXISTS action_tokens CASCADE;") 64 | db = db.Exec("DROP TABLE IF EXISTS audit_events CASCADE;") 65 | db = db.Exec("DROP TABLE IF EXISTS users CASCADE;") 66 | 67 | dataStore.db = db 68 | } 69 | 70 | func (dataStore *DataStore) Sync() { 71 | db := dataStore.db 72 | 73 | db = db.AutoMigrate(&User{}) 74 | db = db.AutoMigrate(&ActionToken{}) 75 | 76 | db = db.AutoMigrate(&FidoToken{}) 77 | db = db.AutoMigrate(&TotpToken{}) 78 | db = db.AutoMigrate(&BackupToken{}) 79 | 80 | db = db.AutoMigrate(&AuditEvent{}) 81 | 82 | db = dataStore.OauthStore.Sync(true) 83 | 84 | dataStore.db = db 85 | } 86 | 87 | // ForceSync Drop and create existing tables to match required schema 88 | // WARNING: do not run this on a live database... 89 | func (dataStore *DataStore) ForceSync() { 90 | dataStore.Drop() 91 | dataStore.Sync() 92 | } 93 | -------------------------------------------------------------------------------- /lib/controllers/datastore/datastore_test.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Datastore - tests 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package datastore 8 | 9 | import ( 10 | "fmt" 11 | "testing" 12 | 13 | "github.com/authplz/authplz-core/lib/config" 14 | ) 15 | 16 | func TestDatastore(t *testing.T) { 17 | 18 | c, _ := config.DefaultConfig() 19 | 20 | // Attempt database connection 21 | ds, err := NewDataStore(c.Database) 22 | if err != nil { 23 | t.Errorf("%s", err) 24 | t.FailNow() 25 | } 26 | defer ds.Close() 27 | 28 | ds.ForceSync() 29 | 30 | var fakeEmail = "test1@abc.com" 31 | var fakePass = "abcDEF123@" 32 | var fakeName = "user.sdfsfdF" 33 | 34 | // Run tests 35 | t.Run("Add user", func(t *testing.T) { 36 | // Create user 37 | u, err := ds.AddUser(fakeEmail, fakeName, fakePass) 38 | if err != nil { 39 | t.Error(err) 40 | return 41 | } 42 | if u == nil { 43 | t.Error("User addition failed") 44 | return 45 | } 46 | 47 | u2, err2 := ds.GetUserByEmail(fakeEmail) 48 | if err2 != nil { 49 | t.Error(err2) 50 | return 51 | } 52 | if u2 == nil { 53 | t.Error("User find by email failed") 54 | return 55 | } 56 | 57 | u2inst := u2.(*User) 58 | 59 | if u2inst.GetEmail() != fakeEmail { 60 | t.Error("Email address mismatch") 61 | return 62 | } 63 | 64 | }) 65 | 66 | t.Run("Rejects users with invalid emails", func(t *testing.T) { 67 | // Create user 68 | _, err := ds.AddUser("abcdef", fakeName, fakePass) 69 | if err == nil { 70 | t.Error("Invalid email allowed") 71 | } 72 | }) 73 | 74 | t.Run("Finds users by email", func(t *testing.T) { 75 | // Create user 76 | u, err := ds.GetUserByEmail(fakeEmail) 77 | if err != nil { 78 | t.Error(err) 79 | return 80 | } 81 | if u == nil { 82 | t.Error("User find by email failed") 83 | return 84 | } 85 | userInst := u.(*User) 86 | 87 | if userInst.GetEmail() != fakeEmail { 88 | t.Error("Email address mismatch") 89 | return 90 | } 91 | }) 92 | 93 | t.Run("Finds users by uuid", func(t *testing.T) { 94 | // Create user 95 | u, err := ds.GetUserByEmail(fakeEmail) 96 | if err != nil { 97 | t.Error(err) 98 | return 99 | } 100 | if u == nil { 101 | t.Error("User find by email failed") 102 | return 103 | } 104 | 105 | userInst := u.(*User) 106 | 107 | u, err = ds.GetUserByExtID(userInst.GetExtID()) 108 | if err != nil { 109 | t.Error(err) 110 | return 111 | } 112 | if u == nil { 113 | t.Error("User find by UserId failed") 114 | return 115 | } 116 | 117 | userInst = u.(*User) 118 | 119 | if userInst.GetEmail() != fakeEmail { 120 | t.Error("Email address mismatch") 121 | return 122 | } 123 | }) 124 | 125 | t.Run("Update users", func(t *testing.T) { 126 | // Create user 127 | u, err := ds.GetUserByEmail(fakeEmail) 128 | if err != nil { 129 | t.Error(err) 130 | return 131 | } 132 | if u == nil { 133 | t.Error("User find by email failed") 134 | return 135 | } 136 | 137 | userInst := u.(*User) 138 | 139 | if userInst.GetPassword() != fakePass { 140 | t.Error("Initial password mismatch") 141 | return 142 | } 143 | 144 | newPassword := "NewPassword" 145 | userInst.SetPassword(newPassword) 146 | 147 | _, err = ds.UpdateUser(userInst) 148 | if err != nil { 149 | t.Error(err) 150 | return 151 | } 152 | 153 | u, err = ds.GetUserByEmail(fakeEmail) 154 | if err != nil { 155 | t.Error(err) 156 | return 157 | } 158 | 159 | userInst = u.(*User) 160 | 161 | if userInst.GetPassword() != newPassword { 162 | t.Error("Initial password mismatch") 163 | return 164 | } 165 | 166 | }) 167 | 168 | t.Run("Add U2F tokens", func(t *testing.T) { 169 | // Create user 170 | u, err := ds.GetUserByEmail(fakeEmail) 171 | if err != nil { 172 | t.Error(err) 173 | return 174 | } 175 | if u == nil { 176 | t.Error("User find by email failed") 177 | return 178 | } 179 | 180 | fidoToken := FidoToken{} 181 | ds.db.Model(u).Association("FidoTokens").Append(fidoToken) 182 | 183 | u, err = ds.GetUserByEmail(fakeEmail) 184 | if err != nil { 185 | t.Error(err) 186 | return 187 | } 188 | 189 | ds.GetTokens(u) 190 | 191 | fmt.Printf("%+v", u) 192 | 193 | userInst := u.(*User) 194 | 195 | if userInst.SecondFactors() == false { 196 | t.Error("No second factors found") 197 | return 198 | } 199 | }) 200 | 201 | // Tear down user controller 202 | 203 | } 204 | -------------------------------------------------------------------------------- /lib/controllers/datastore/fidotoken.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Datastore - 2fa fido / u2f tokens 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package datastore 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/jinzhu/gorm" 13 | "github.com/satori/go.uuid" 14 | ) 15 | 16 | // FidoToken Fido/U2F token object 17 | type FidoToken struct { 18 | gorm.Model 19 | ExtID string 20 | UserID uint 21 | Name string 22 | KeyHandle string 23 | PublicKey string 24 | Certificate string 25 | Counter uint 26 | LastUsed time.Time 27 | } 28 | 29 | // Getters and setters for external interface compliance 30 | 31 | // GetName fetches the token Name 32 | func (token *FidoToken) GetName() string { return token.Name } 33 | 34 | // GetExtID fetches the external ID for a token 35 | func (token *FidoToken) GetExtID() string { return token.ExtID } 36 | 37 | // GetKeyHandle fetches the token KeyHandle 38 | func (token *FidoToken) GetKeyHandle() string { return token.KeyHandle } 39 | 40 | // GetPublicKey fetches the token PublicKey 41 | func (token *FidoToken) GetPublicKey() string { return token.PublicKey } 42 | 43 | // GetCertificate fetches the token Certificate 44 | func (token *FidoToken) GetCertificate() string { return token.Certificate } 45 | 46 | // GetCounter fetches the token usage counter 47 | func (token *FidoToken) GetCounter() uint { return token.Counter } 48 | 49 | // SetCounter Sets the token usage counter 50 | func (token *FidoToken) SetCounter(count uint) { token.Counter = count } 51 | 52 | // GetLastUsed fetches the token LastUsed time 53 | func (token *FidoToken) GetLastUsed() time.Time { return token.LastUsed } 54 | 55 | // SetLastUsed sets the token LastUsed time 56 | func (token *FidoToken) SetLastUsed(used time.Time) { token.LastUsed = used } 57 | 58 | // AddFidoToken creates a fido token instance in the database 59 | func (dataStore *DataStore) AddFidoToken(userid, name, keyHandle, publicKey, certificate string, counter uint) (interface{}, error) { 60 | 61 | // Fetch user 62 | u, err := dataStore.GetUserByExtID(userid) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | user := u.(*User) 68 | 69 | // Create a token instance 70 | token := FidoToken{ 71 | ExtID: uuid.NewV4().String(), 72 | UserID: user.ID, 73 | Name: name, 74 | KeyHandle: keyHandle, 75 | PublicKey: publicKey, 76 | Certificate: certificate, 77 | Counter: counter, 78 | LastUsed: time.Now(), 79 | } 80 | 81 | // Add the token to the user and save 82 | user.FidoTokens = append(user.FidoTokens, token) 83 | _, err = dataStore.UpdateUser(user) 84 | return user, err 85 | } 86 | 87 | // GetFidoTokens fetches the fido tokens for a provided user 88 | func (dataStore *DataStore) GetFidoTokens(userid string) ([]interface{}, error) { 89 | var fidoTokens []FidoToken 90 | 91 | // Fetch user 92 | u, err := dataStore.GetUserByExtID(userid) 93 | if err != nil { 94 | return nil, err 95 | } 96 | if u == nil { 97 | return nil, ErrUserNotFound 98 | } 99 | 100 | err = dataStore.db.Model(u).Related(&fidoTokens).Error 101 | 102 | interfaces := make([]interface{}, len(fidoTokens)) 103 | for i, t := range fidoTokens { 104 | interfaces[i] = &t 105 | } 106 | 107 | return interfaces, err 108 | } 109 | 110 | // UpdateFidoToken updates a fido token instance 111 | func (dataStore *DataStore) UpdateFidoToken(token interface{}) (interface{}, error) { 112 | err := dataStore.db.Save(token).Error 113 | if err != nil { 114 | return nil, err 115 | } 116 | return token, nil 117 | } 118 | 119 | // RemoveFidoToken deletes a totp token 120 | func (dataStore *DataStore) RemoveFidoToken(token interface{}) error { 121 | return dataStore.db.Delete(token).Error 122 | } 123 | -------------------------------------------------------------------------------- /lib/controllers/datastore/oauth2/access_token.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * OAuth data store - access tokens 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package oauthstore 8 | 9 | import ( 10 | "github.com/jinzhu/gorm" 11 | "time" 12 | ) 13 | 14 | // OauthAccessToken Oauth Access token session 15 | type OauthAccessToken struct { 16 | gorm.Model 17 | UserID uint 18 | ClientID uint 19 | Signature string 20 | OauthRequest 21 | OauthSession 22 | } 23 | 24 | func (oa *OauthAccessToken) GetSignature() string { return oa.Signature } 25 | 26 | func (oa *OauthAccessToken) GetSession() interface{} { return &oa.OauthSession } 27 | 28 | func (oa *OauthAccessToken) SetSession(session interface{}) { 29 | // I don't even know what to do here 30 | } 31 | 32 | func (os *OauthStore) AddAccessTokenSession(userID, clientID, signature, requestID string, 33 | requestedAt, expiresAt time.Time, requestedScopes, grantedScopes []string) (interface{}, error) { 34 | 35 | u, err := os.base.GetUserByExtID(userID) 36 | if err != nil { 37 | return nil, err 38 | } 39 | user := u.(User) 40 | 41 | c, err := os.GetClientByID(clientID) 42 | if err != nil { 43 | return nil, err 44 | } 45 | client := c.(*OauthClient) 46 | 47 | request := OauthRequest{ 48 | RequestID: requestID, 49 | RequestedAt: requestedAt, 50 | ExpiresAt: expiresAt, 51 | } 52 | request.SetRequestedScopes(requestedScopes) 53 | request.SetGrantedScopes(grantedScopes) 54 | 55 | session := OauthSession{ 56 | UserExtID: user.GetExtID(), 57 | Username: user.GetUsername(), 58 | AccessExpiry: expiresAt, 59 | } 60 | 61 | oa := OauthAccessToken{ 62 | ClientID: client.ID, 63 | UserID: user.GetIntID(), 64 | Signature: signature, 65 | OauthRequest: request, 66 | OauthSession: session, 67 | } 68 | 69 | os.db = os.db.Create(&oa) 70 | err = os.db.Error 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | oa.Client = *client 76 | 77 | return &oa, nil 78 | } 79 | 80 | func (os *OauthStore) fetchAccessTokenSession(match *OauthAccessToken) (interface{}, error) { 81 | var accessToken OauthAccessToken 82 | err := os.db.Where(match).First(&accessToken).Error 83 | if (err != nil) && (err != gorm.ErrRecordNotFound) { 84 | return nil, err 85 | } else if (err != nil) && (err == gorm.ErrRecordNotFound) { 86 | return nil, nil 87 | } 88 | 89 | err = os.db.Where(&OauthClient{ID: accessToken.ClientID}).First(&accessToken.Client).Error 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | return &accessToken, nil 95 | } 96 | 97 | // GetAccessTokenSession Fetch a client from an access token 98 | func (os *OauthStore) GetAccessTokenSession(signature string) (interface{}, error) { 99 | return os.fetchAccessTokenSession(&OauthAccessToken{Signature: signature}) 100 | } 101 | 102 | // GetAccessTokenSessionByRequestID fetch an access token by refresh id 103 | func (os *OauthStore) GetAccessTokenSessionByRequestID(requestID string) (interface{}, error) { 104 | return os.fetchAccessTokenSession(&OauthAccessToken{OauthRequest: OauthRequest{RequestID: requestID}}) 105 | } 106 | 107 | // GetAccessTokenSessionsByUserID by a user id 108 | func (os *OauthStore) GetAccessTokenSessionsByUserID(userID string) ([]interface{}, error) { 109 | var oa []OauthAccessToken 110 | err := os.db.Where(&OauthAccessToken{OauthSession: OauthSession{UserExtID: userID}}).Find(&oa).Error 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | interfaces := make([]interface{}, len(oa)) 116 | for i := range oa { 117 | interfaces[i] = &oa[i] 118 | } 119 | 120 | return interfaces, err 121 | } 122 | 123 | // GetClientByAccessTokenSession Fetch a client from an access token 124 | func (os *OauthStore) GetClientByAccessTokenSession(signature string) (interface{}, error) { 125 | var oa OauthAccessToken 126 | err := os.db.Where(&OauthAccessToken{Signature: signature}).First(&oa).Error 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | var oc OauthClient 132 | err = os.db.Where(&OauthClient{ID: oa.ClientID}).First(&oc).Error 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | return &oc, nil 138 | } 139 | 140 | // RemoveAccessTokenSession Remove an access token by session key 141 | func (os *OauthStore) RemoveAccessTokenSession(signature string) error { 142 | err := os.db.Delete(&OauthAccessToken{Signature: signature}).Error 143 | return err 144 | } 145 | -------------------------------------------------------------------------------- /lib/controllers/datastore/oauth2/authorize_code.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * OAuth data store - authorization code 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package oauthstore 8 | 9 | import ( 10 | "github.com/jinzhu/gorm" 11 | "time" 12 | ) 13 | 14 | // OauthAuthorizeCode Authorization data 15 | type OauthAuthorizeCode struct { 16 | gorm.Model 17 | ClientID uint 18 | UserID uint 19 | Code string // Authorization code 20 | Challenge string // Optional code_challenge as described in rfc7636 21 | ChallengeMethod string // Optional code_challenge_method as described in rfc7636 22 | OauthRequest 23 | OauthSession 24 | } 25 | 26 | func (oa *OauthAuthorizeCode) GetCode() string { return oa.Code } 27 | 28 | func (oa *OauthAuthorizeCode) GetSession() interface{} { return &oa.OauthSession } 29 | 30 | func (oa *OauthAuthorizeCode) SetSession(session interface{}) { 31 | // I don't even know what to do here 32 | } 33 | 34 | // AddAuthorizeCodeSession creates an authorization code session in the database 35 | func (oauthStore *OauthStore) AddAuthorizeCodeSession(userID, clientID, code, requestID string, 36 | requestedAt, expiresAt time.Time, requestedScopes, grantedScopes []string) (interface{}, error) { 37 | 38 | u, err := oauthStore.base.GetUserByExtID(userID) 39 | if err != nil { 40 | return nil, err 41 | } 42 | user := u.(User) 43 | 44 | c, err := oauthStore.GetClientByID(clientID) 45 | if err != nil { 46 | return nil, err 47 | } 48 | client := c.(*OauthClient) 49 | 50 | or := OauthRequest{ 51 | RequestID: requestID, 52 | RequestedAt: requestedAt, 53 | } 54 | 55 | or.SetRequestedScopes(requestedScopes) 56 | or.SetGrantedScopes(grantedScopes) 57 | 58 | session := NewSession(user.GetExtID(), user.GetUsername()) 59 | session.AuthorizeExpiry = expiresAt 60 | 61 | authorize := OauthAuthorizeCode{ 62 | ClientID: client.ID, 63 | UserID: user.GetIntID(), 64 | Code: code, 65 | OauthRequest: or, 66 | OauthSession: session, 67 | } 68 | 69 | oauthStore.db = oauthStore.db.Create(&authorize) 70 | err = oauthStore.db.Error 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | authorize.Client = *client 76 | 77 | return &authorize, nil 78 | } 79 | 80 | func (oauthStore *OauthStore) fetchAuthorizeCodeSession(match *OauthAuthorizeCode) (interface{}, error) { 81 | var authorize OauthAuthorizeCode 82 | err := oauthStore.db.Where(match).First(&authorize).Error 83 | if (err != nil) && (err != gorm.ErrRecordNotFound) { 84 | return nil, err 85 | } else if (err != nil) && (err == gorm.ErrRecordNotFound) { 86 | return nil, nil 87 | } 88 | 89 | err = oauthStore.db.Where(&OauthClient{ID: authorize.ClientID}).First(&authorize.Client).Error 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | return &authorize, nil 95 | } 96 | 97 | // GetAuthorizeCodeSession fetches an authorization code session 98 | func (oauthStore *OauthStore) GetAuthorizeCodeSession(code string) (interface{}, error) { 99 | return oauthStore.fetchAuthorizeCodeSession(&OauthAuthorizeCode{Code: code}) 100 | } 101 | 102 | // GetAuthorizeCodeSessionByRequestID fetches an authorization code session by the originator request ID 103 | func (oauthStore *OauthStore) GetAuthorizeCodeSessionByRequestID(requestID string) (interface{}, error) { 104 | return oauthStore.fetchAuthorizeCodeSession(&OauthAuthorizeCode{OauthRequest: OauthRequest{RequestID: requestID}}) 105 | } 106 | 107 | func (os *OauthStore) GetAuthorizeCodeSessionsByUserID(userID string) ([]interface{}, error) { 108 | var codes []OauthAuthorizeCode 109 | err := os.db.Where(&OauthAuthorizeCode{OauthSession: OauthSession{UserExtID: userID}}).Find(&codes).Error 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | interfaces := make([]interface{}, len(codes)) 115 | for i := range codes { 116 | interfaces[i] = &codes[i] 117 | } 118 | 119 | return interfaces, err 120 | } 121 | 122 | // RemoveAuthorizeCodeSession removes an authorization code session using the provided code 123 | func (oauthStore *OauthStore) RemoveAuthorizeCodeSession(code string) error { 124 | authorization := OauthAuthorizeCode{ 125 | Code: code, 126 | } 127 | 128 | oauthStore.db = oauthStore.db.Delete(&authorization) 129 | 130 | return oauthStore.db.Error 131 | } 132 | -------------------------------------------------------------------------------- /lib/controllers/datastore/oauth2/client.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * OAuth data store - clients 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package oauthstore 8 | 9 | import ( 10 | "fmt" 11 | "github.com/jinzhu/gorm" 12 | "time" 13 | ) 14 | 15 | // OauthClient is a client application registration 16 | type OauthClient struct { 17 | ID uint `gorm:"primary_key" description:"Internal Database ID"` 18 | CreatedAt time.Time `description:"Creation time"` 19 | UpdatedAt time.Time `description:"Last update time"` 20 | ClientID string `gorm:"unique"` 21 | Name string `gorm:"unique"` 22 | UserID uint 23 | LastUsed time.Time 24 | Secret string 25 | 26 | Scopes string 27 | RedirectURIs string 28 | GrantTypes string 29 | ResponseTypes string 30 | 31 | UserData string 32 | Public bool 33 | } 34 | 35 | func (c *OauthClient) GetID() string { return c.ClientID } 36 | func (c *OauthClient) GetName() string { return c.Name } 37 | func (c *OauthClient) GetSecret() string { return c.Secret } 38 | 39 | func (c *OauthClient) GetUserData() interface{} { return c.UserData } 40 | func (c *OauthClient) GetLastUsed() time.Time { return c.LastUsed } 41 | func (c *OauthClient) GetCreatedAt() time.Time { return c.CreatedAt } 42 | func (c *OauthClient) IsPublic() bool { return c.Public } 43 | 44 | func (c *OauthClient) SetID(id string) { c.ClientID = id } 45 | func (c *OauthClient) SetLastUsed(t time.Time) { c.LastUsed = t } 46 | 47 | func (c *OauthClient) SetSecret(secret string) { c.Secret = secret } 48 | func (c *OauthClient) SetUserData(userData string) { c.UserData = userData } 49 | 50 | func (c *OauthClient) GetRedirectURIs() []string { 51 | return stringToArray(c.RedirectURIs) 52 | } 53 | func (c *OauthClient) GetGrantTypes() []string { 54 | return stringToArray(c.GrantTypes) 55 | } 56 | func (c *OauthClient) GetResponseTypes() []string { 57 | return stringToArray(c.ResponseTypes) 58 | } 59 | func (c *OauthClient) GetScopes() []string { 60 | return stringToArray(c.Scopes) 61 | } 62 | 63 | func (c *OauthClient) SetRedirectURIs(redirectURIs []string) { 64 | c.RedirectURIs = arrayToString(redirectURIs) 65 | } 66 | func (c *OauthClient) SetGrantTypes(grantTypes []string) { 67 | c.GrantTypes = arrayToString(grantTypes) 68 | } 69 | func (c *OauthClient) SetResponseTypes(responseTypes []string) { 70 | c.ResponseTypes = arrayToString(responseTypes) 71 | } 72 | func (c *OauthClient) SetScopes(scopes []string) { 73 | c.Scopes = arrayToString(scopes) 74 | } 75 | 76 | // AddClient adds an OAuth2 client application to the database 77 | func (oauthStore *OauthStore) AddClient(userID, clientID, clientName, secret string, 78 | scopes, redirects, grantTypes, responseTypes []string, public bool) (interface{}, error) { 79 | // Fetch user 80 | u, err := oauthStore.base.GetUserByExtID(userID) 81 | if err != nil { 82 | return nil, err 83 | } 84 | if u == nil { 85 | return nil, fmt.Errorf("No user account found for userID: %s", userID) 86 | } 87 | user := u.(User) 88 | 89 | // Create Client object 90 | client := OauthClient{ 91 | UserID: user.GetIntID(), 92 | ClientID: clientID, 93 | Name: clientName, 94 | CreatedAt: time.Now(), 95 | LastUsed: time.Now(), 96 | Secret: secret, 97 | } 98 | client.SetScopes(scopes) 99 | client.SetRedirectURIs(redirects) 100 | client.SetGrantTypes(grantTypes) 101 | client.SetResponseTypes(responseTypes) 102 | 103 | // Save to store 104 | oauthStore.db = oauthStore.db.Create(&client) 105 | err = oauthStore.db.Error 106 | if err != nil { 107 | return nil, err 108 | } 109 | return &client, nil 110 | } 111 | 112 | // GetClientByID an oauth client app by ClientID 113 | func (oauthStore *OauthStore) GetClientByID(clientID string) (interface{}, error) { 114 | var client OauthClient 115 | err := oauthStore.db.Where(&OauthClient{ClientID: clientID}).First(&client).Error 116 | if (err != nil) && (err != gorm.ErrRecordNotFound) { 117 | return nil, err 118 | } else if (err != nil) && (err == gorm.ErrRecordNotFound) { 119 | return nil, nil 120 | } 121 | 122 | return &client, nil 123 | } 124 | 125 | // GetClientsByUserID fetches the OauthClients for a provided userID 126 | func (oauthStore *OauthStore) GetClientsByUserID(userID string) ([]interface{}, error) { 127 | var oauthClients []OauthClient 128 | 129 | // Fetch user 130 | u, err := oauthStore.base.GetUserByExtID(userID) 131 | if err != nil { 132 | return nil, err 133 | } 134 | if u == nil { 135 | return nil, fmt.Errorf("No user account found for userID: %s", userID) 136 | } 137 | //user := u.(*User) 138 | 139 | err = oauthStore.db.Model(u).Related(&oauthClients).Error 140 | 141 | interfaces := make([]interface{}, len(oauthClients)) 142 | for i, t := range oauthClients { 143 | interfaces[i] = &t 144 | } 145 | 146 | return interfaces, err 147 | } 148 | 149 | // UpdateClient Update a user object 150 | func (oauthStore *OauthStore) UpdateClient(client interface{}) (interface{}, error) { 151 | c := client.(*OauthClient) 152 | 153 | err := oauthStore.db.Save(&c).Error 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | return client, nil 159 | } 160 | 161 | // RemoveClientByID removes a client application by id 162 | func (oauthStore *OauthStore) RemoveClientByID(clientID string) error { 163 | client := OauthClient{ 164 | ClientID: clientID, 165 | } 166 | 167 | oauthStore.db = oauthStore.db.Delete(&client) 168 | 169 | return oauthStore.db.Error 170 | } 171 | -------------------------------------------------------------------------------- /lib/controllers/datastore/oauth2/oauth.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * OAuth data store - core 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package oauthstore 8 | 9 | import ( 10 | "bytes" 11 | "encoding/gob" 12 | "encoding/json" 13 | "github.com/jinzhu/gorm" 14 | ) 15 | 16 | func init() { 17 | // Register database objects for future serialisation if required 18 | gob.Register(&OauthClient{}) 19 | gob.Register(&OauthSession{}) 20 | gob.Register(&OauthAuthorizeCode{}) 21 | gob.Register(&OauthAccessToken{}) 22 | gob.Register(&OauthRefreshToken{}) 23 | } 24 | 25 | // User defines the user interface required by the Oauth2 storage module 26 | type User interface { 27 | GetIntID() uint 28 | GetExtID() string 29 | GetUsername() string 30 | } 31 | 32 | // BaseStore is the interface required by the oauth module for underlying storage 33 | // This defines required non-oauth methods 34 | type BaseStore interface { 35 | GetUserByExtID(string) (interface{}, error) 36 | } 37 | 38 | // OauthStore is a storage instance for OAuth components 39 | type OauthStore struct { 40 | db *gorm.DB 41 | base BaseStore 42 | } 43 | 44 | // NewOauthStore creates an oauthstore from a provided gorm.DB and baseStore instance 45 | func NewOauthStore(db *gorm.DB, baseStore BaseStore) *OauthStore { 46 | return &OauthStore{db, baseStore} 47 | } 48 | 49 | // Sync drops and rebuilds existing OAuth tables 50 | func Sync(dataStore *gorm.DB) *gorm.DB { 51 | db := dataStore 52 | 53 | db = db.Exec("DROP TABLE IF EXISTS oauth_clients CASCADE;") 54 | db = db.Exec("DROP TABLE IF EXISTS oauth_authorize CASCADE;") 55 | db = db.Exec("DROP TABLE IF EXISTS oauth_sessions CASCADE;") 56 | db = db.Exec("DROP TABLE IF EXISTS oauth_access_tokens CASCADE;") 57 | db = db.Exec("DROP TABLE IF EXISTS oauth_authorize_codes CASCADE;") 58 | db = db.Exec("DROP TABLE IF EXISTS oauth_refresh_tokens CASCADE;") 59 | 60 | db = db.AutoMigrate(&OauthClient{}) 61 | db = db.AutoMigrate(&OauthAuthorizeCode{}) 62 | db = db.AutoMigrate(&OauthAccessToken{}) 63 | db = db.AutoMigrate(&OauthRefreshToken{}) 64 | 65 | return db 66 | } 67 | 68 | // Sync Synchronizes the database 69 | // Force causes existing table to be dropped 70 | func (os *OauthStore) Sync(force bool) *gorm.DB { 71 | return Sync(os.db) 72 | } 73 | 74 | func stringToArray(str string) []string { 75 | buf := bytes.NewBuffer([]byte(str)) 76 | var arr []string 77 | 78 | json.NewDecoder(buf).Decode(&arr) 79 | 80 | return arr 81 | } 82 | 83 | func arrayToString(arr []string) string { 84 | var buf bytes.Buffer 85 | 86 | json.NewEncoder(&buf).Encode(&arr) 87 | 88 | return buf.String() 89 | } 90 | -------------------------------------------------------------------------------- /lib/controllers/datastore/oauth2/refresh_token.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * OAuth data store - refresh tokens 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package oauthstore 8 | 9 | import ( 10 | "time" 11 | ) 12 | 13 | import ( 14 | "github.com/jinzhu/gorm" 15 | ) 16 | 17 | // OauthRefreshToken Refresh token storage 18 | type OauthRefreshToken struct { 19 | gorm.Model 20 | UserID uint 21 | ClientID uint 22 | Signature string 23 | OauthRequest 24 | OauthSession 25 | } 26 | 27 | // GetSignature fetches the Refresh token signature 28 | func (or *OauthRefreshToken) GetSignature() string { return or.Signature } 29 | 30 | func (or *OauthRefreshToken) GetSession() interface{} { return &or.OauthSession } 31 | 32 | func (or *OauthRefreshToken) SetSession(session interface{}) { 33 | // I don't even know what to do here 34 | } 35 | 36 | // AddRefreshTokenSession creates a refresh token session in the database 37 | func (os *OauthStore) AddRefreshTokenSession(userID, clientID, signature, requestID string, 38 | requestedAt, expiresAt time.Time, requestedScopes, grantedScopes []string) (interface{}, error) { 39 | 40 | u, err := os.base.GetUserByExtID(userID) 41 | if err != nil { 42 | return nil, err 43 | } 44 | user := u.(User) 45 | 46 | c, err := os.GetClientByID(clientID) 47 | if err != nil { 48 | return nil, err 49 | } 50 | client := c.(*OauthClient) 51 | 52 | request := OauthRequest{ 53 | RequestID: requestID, 54 | RequestedAt: time.Now(), 55 | } 56 | request.SetRequestedScopes(requestedScopes) 57 | request.SetGrantedScopes(grantedScopes) 58 | 59 | session := NewSession(user.GetExtID(), user.GetUsername()) 60 | session.RefreshExpiry = expiresAt 61 | 62 | oa := OauthRefreshToken{ 63 | ClientID: client.ID, 64 | UserID: user.GetIntID(), 65 | Signature: signature, 66 | OauthRequest: request, 67 | OauthSession: session, 68 | } 69 | 70 | os.db = os.db.Create(&oa) 71 | err = os.db.Error 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | oa.Client = *client 77 | 78 | return &oa, nil 79 | } 80 | 81 | func (os *OauthStore) fetchRefreshTokenSession(match *OauthRefreshToken) (interface{}, error) { 82 | var refreshToken OauthRefreshToken 83 | err := os.db.Where(match).First(&refreshToken).Error 84 | if (err != nil) && (err != gorm.ErrRecordNotFound) { 85 | return nil, err 86 | } else if (err != nil) && (err == gorm.ErrRecordNotFound) { 87 | return nil, nil 88 | } 89 | 90 | err = os.db.Where(&OauthClient{ID: refreshToken.ClientID}).First(&refreshToken.Client).Error 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | return &refreshToken, nil 96 | } 97 | 98 | // Fetch a client from an access token 99 | func (os *OauthStore) GetRefreshTokenBySignature(signature string) (interface{}, error) { 100 | return os.fetchRefreshTokenSession(&OauthRefreshToken{Signature: signature}) 101 | } 102 | 103 | func (os *OauthStore) GetRefreshTokenSessionByRequestID(requestID string) (interface{}, error) { 104 | return os.fetchRefreshTokenSession(&OauthRefreshToken{OauthRequest: OauthRequest{RequestID: requestID}}) 105 | } 106 | 107 | func (os *OauthStore) GetRefreshTokenSessionsByUserID(userID string) ([]interface{}, error) { 108 | var refreshes []OauthRefreshToken 109 | err := os.db.Where(&OauthRefreshToken{OauthSession: OauthSession{UserExtID: userID}}).Find(&refreshes).Error 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | interfaces := make([]interface{}, len(refreshes)) 115 | for i := range refreshes { 116 | interfaces[i] = &refreshes[i] 117 | } 118 | 119 | return interfaces, err 120 | } 121 | 122 | // Fetch a client from an access token 123 | func (os *OauthStore) GetClientByRefreshToken(signature string) (interface{}, error) { 124 | var refresh OauthRefreshToken 125 | err := os.db.Where(&OauthRefreshToken{Signature: signature}).First(&refresh).Error 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | var client OauthClient 131 | err = os.db.Where(&OauthClient{ID: refresh.ClientID}).First(&client).Error 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return &client, nil 137 | } 138 | 139 | func (os *OauthStore) RemoveRefreshToken(signature string) error { 140 | err := os.db.Delete(&OauthRefreshToken{Signature: signature}).Error 141 | return err 142 | } 143 | -------------------------------------------------------------------------------- /lib/controllers/datastore/oauth2/request.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * OAuth data store - requests 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package oauthstore 8 | 9 | import ( 10 | "time" 11 | ) 12 | 13 | // OauthRequest Base Type 14 | // This is not stored directly, but used in other oauth types 15 | type OauthRequest struct { 16 | RequestID string 17 | RequestedAt time.Time 18 | ExpiresAt time.Time 19 | RequestedScopes string 20 | GrantedScopes string 21 | Form string 22 | Client OauthClient `sql:"-"` 23 | Session OauthSession `sql:"-"` 24 | } 25 | 26 | // Getters and Setters 27 | 28 | func (or *OauthRequest) GetRequestID() string { 29 | return or.RequestID 30 | } 31 | 32 | func (or *OauthRequest) SetRequestID(id string) { 33 | or.RequestID = id 34 | } 35 | 36 | func (or *OauthRequest) GetClient() interface{} { 37 | return &or.Client 38 | } 39 | 40 | func (or *OauthRequest) GetSession() interface{} { 41 | return &or.Session 42 | } 43 | 44 | func (or *OauthRequest) GetRequestedAt() time.Time { 45 | return or.RequestedAt 46 | } 47 | 48 | func (or *OauthRequest) GetExpiresAt() time.Time { 49 | return or.ExpiresAt 50 | } 51 | 52 | func (c *OauthRequest) GetRequestedScopes() []string { 53 | return stringToArray(c.RequestedScopes) 54 | } 55 | 56 | func (c *OauthRequest) SetRequestedScopes(scopes []string) { 57 | c.RequestedScopes = arrayToString(scopes) 58 | } 59 | 60 | func (c *OauthRequest) AppendRequestedScope(scope string) { 61 | c.SetRequestedScopes(append(c.GetRequestedScopes(), scope)) 62 | } 63 | 64 | func (c *OauthRequest) GetGrantedScopes() []string { 65 | return stringToArray(c.GrantedScopes) 66 | } 67 | 68 | func (c *OauthRequest) SetGrantedScopes(scopes []string) { 69 | c.GrantedScopes = arrayToString(scopes) 70 | } 71 | 72 | func (c *OauthRequest) GrantScope(scope string) { 73 | c.SetGrantedScopes(append(c.GetGrantedScopes(), scope)) 74 | } 75 | 76 | func (c *OauthRequest) Merge(a interface{}) { 77 | request := a.(*OauthRequest) 78 | 79 | for _, scope := range request.GetRequestedScopes() { 80 | c.AppendRequestedScope(scope) 81 | } 82 | for _, scope := range request.GetGrantedScopes() { 83 | c.GrantScope(scope) 84 | } 85 | c.RequestedAt = request.GetRequestedAt() 86 | c.Client = *request.GetClient().(*OauthClient) 87 | c.Session = *request.GetSession().(*OauthSession) 88 | 89 | /* 90 | for k, v := range request.GetRequestForm() { 91 | c.Form[k] = v 92 | } 93 | */ 94 | } 95 | -------------------------------------------------------------------------------- /lib/controllers/datastore/oauth2/session.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * OAuth data store - oauth sessions 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package oauthstore 8 | 9 | import ( 10 | "bytes" 11 | "encoding/gob" 12 | "time" 13 | ) 14 | 15 | func init() { 16 | gob.Register(&OauthSession{}) 17 | } 18 | 19 | // OauthSession session storage base type 20 | // Used by grants for session storage 21 | type OauthSession struct { 22 | UserExtID string 23 | Username string 24 | Subject string 25 | AccessExpiry time.Time 26 | RefreshExpiry time.Time 27 | AuthorizeExpiry time.Time 28 | IDExpiry time.Time 29 | } 30 | 31 | // NewSession creates an OauthSession 32 | func NewSession(userID, username string) OauthSession { 33 | return OauthSession{ 34 | UserExtID: userID, 35 | Username: username, 36 | } 37 | } 38 | 39 | func (s *OauthSession) GetSession() interface{} { return s } 40 | 41 | // Getters and Setters 42 | 43 | func (s *OauthSession) GetUserID() string { return s.UserExtID } 44 | func (s *OauthSession) GetUsername() string { return s.Username } 45 | func (s *OauthSession) GetSubject() string { return s.Subject } 46 | func (s *OauthSession) SetAccessExpiry(t time.Time) { s.AccessExpiry = t } 47 | func (s *OauthSession) GetAccessExpiry() time.Time { return s.AccessExpiry } 48 | func (s *OauthSession) SetRefreshExpiry(t time.Time) { s.RefreshExpiry = t } 49 | func (s *OauthSession) GetRefreshExpiry() time.Time { return s.RefreshExpiry } 50 | func (s *OauthSession) SetAuthorizeExpiry(t time.Time) { s.AuthorizeExpiry = t } 51 | func (s *OauthSession) GetAuthorizeExpiry() time.Time { return s.AuthorizeExpiry } 52 | func (s *OauthSession) SetIDExpiry(t time.Time) { s.IDExpiry = t } 53 | func (s *OauthSession) GetIDExpiry() time.Time { return s.IDExpiry } 54 | 55 | func (s *OauthSession) Clone() interface{} { 56 | clone := OauthSession{} 57 | 58 | var buf bytes.Buffer 59 | enc := gob.NewEncoder(&buf) 60 | dec := gob.NewDecoder(&buf) 61 | _ = enc.Encode(s) 62 | _ = dec.Decode(&clone) 63 | 64 | return &clone 65 | } 66 | -------------------------------------------------------------------------------- /lib/controllers/datastore/totptoken.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Datastore - 2fa totp tokens 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package datastore 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/jinzhu/gorm" 13 | "github.com/satori/go.uuid" 14 | ) 15 | 16 | // TotpToken Time based One Time Password Token object 17 | type TotpToken struct { 18 | gorm.Model 19 | ExtID string 20 | UserID uint 21 | Name string 22 | Secret string 23 | UsageCount uint 24 | LastUsed time.Time 25 | } 26 | 27 | // Getters and setters for external interface compliance 28 | 29 | // GetName fetches the fido token Name 30 | func (token *TotpToken) GetName() string { return token.Name } 31 | 32 | // GetExtID fetches the external ID for a token 33 | func (token *TotpToken) GetExtID() string { return token.ExtID } 34 | 35 | // GetSecret fetches the fido token Secret 36 | func (token *TotpToken) GetSecret() string { return token.Secret } 37 | 38 | // GetCounter fetches the fido token Counter 39 | func (token *TotpToken) GetCounter() uint { return token.UsageCount } 40 | 41 | // SetCounter sets the fido token usage counter 42 | func (token *TotpToken) SetCounter(count uint) { token.UsageCount = count } 43 | 44 | // GetLastUsed fetches the fido token LastUsed time 45 | func (token *TotpToken) GetLastUsed() time.Time { return token.LastUsed } 46 | 47 | // SetLastUsed sets the fido token LastUsed time 48 | func (token *TotpToken) SetLastUsed(used time.Time) { token.LastUsed = used } 49 | 50 | // AddTotpToken adds a TOTP token to the provided user 51 | func (ds *DataStore) AddTotpToken(userid, name, secret string, counter uint) (interface{}, error) { 52 | // Fetch user 53 | u, err := ds.GetUserByExtID(userid) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | user := u.(*User) 59 | totpToken := TotpToken{ 60 | UserID: user.ID, 61 | ExtID: uuid.NewV4().String(), 62 | Name: name, 63 | Secret: secret, 64 | UsageCount: counter, 65 | LastUsed: time.Now(), 66 | } 67 | 68 | user.TotpTokens = append(user.TotpTokens, totpToken) 69 | _, err = ds.UpdateUser(user) 70 | return &totpToken, err 71 | } 72 | 73 | // GetTotpTokens fetches tokens attached to a given user 74 | func (ds *DataStore) GetTotpTokens(userid string) ([]interface{}, error) { 75 | var totpTokens []TotpToken 76 | 77 | // Fetch user 78 | u, err := ds.GetUserByExtID(userid) 79 | if err != nil { 80 | return nil, err 81 | } 82 | user := u.(*User) 83 | 84 | // Grab tokens 85 | err = ds.db.Model(user).Related(&totpTokens).Error 86 | 87 | interfaces := make([]interface{}, len(totpTokens)) 88 | for i, t := range totpTokens { 89 | interfaces[i] = &t 90 | } 91 | 92 | return interfaces, err 93 | } 94 | 95 | // UpdateTotpToken updates a TOTP token instance in the database 96 | func (ds *DataStore) UpdateTotpToken(token interface{}) (interface{}, error) { 97 | err := ds.db.Save(token).Error 98 | if err != nil { 99 | return nil, err 100 | } 101 | return token, nil 102 | } 103 | 104 | // RemoveTotpToken deletes a totp token 105 | func (ds *DataStore) RemoveTotpToken(token interface{}) error { 106 | return ds.db.Delete(token).Error 107 | } 108 | -------------------------------------------------------------------------------- /lib/controllers/mailer/drivers/logger.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Logging mailer driver 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package drivers 8 | 9 | import ( 10 | "log" 11 | ) 12 | 13 | const ( 14 | LoggerDriverID = "logger" 15 | ) 16 | 17 | // LoggerDriver is a mailer driver that writes mail to logs (for testing use only) 18 | type LoggerDriver struct { 19 | options map[string]string 20 | } 21 | 22 | // NewLoggerDriver creates a new mailgun driver instance 23 | func NewLoggerDriver(options map[string]string) (*LoggerDriver, error) { 24 | return &LoggerDriver{options}, nil 25 | } 26 | 27 | // Send writes a message to the console 28 | func (md *LoggerDriver) Send(address, subject, body string) error { 29 | if m, ok := md.options["mode"]; ok && m == "silent" { 30 | return nil 31 | } 32 | log.Printf("Mailer.LoggerDriver Send To: %s Subject: %s\n%s", address, subject, body) 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /lib/controllers/mailer/drivers/mailgun.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Mailgun mailer driver 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | 7 | package drivers 8 | 9 | import ( 10 | "log" 11 | 12 | "fmt" 13 | "gopkg.in/mailgun/mailgun-go.v1" 14 | ) 15 | 16 | const ( 17 | MailgunDriverID = "mailgun" 18 | ) 19 | 20 | // MailgunDriver is a mailer driver using the mailgun API 21 | type MailgunDriver struct { 22 | domain string 23 | from string 24 | key string 25 | secret string 26 | mg mailgun.Mailgun 27 | testMode bool 28 | } 29 | 30 | // NewMailgunDriver creates a new mailgun driver instance 31 | func NewMailgunDriver(options map[string]string) (*MailgunDriver, error) { 32 | 33 | domain, ok := options["domain"] 34 | if !ok || domain == "" { 35 | return nil, fmt.Errorf("MailgunDriver options requires a 'domain' argument") 36 | } 37 | address, ok := options["address"] 38 | if !ok || address == "" { 39 | return nil, fmt.Errorf("MailgunDriver options requires a 'address' address argument") 40 | } 41 | APIKey, ok := options["key"] 42 | if !ok || APIKey == "" { 43 | return nil, fmt.Errorf("MailgunDriver options requires a 'key' argument") 44 | } 45 | APISecret, ok := options["secret"] 46 | if !ok || APISecret == "" { 47 | return nil, fmt.Errorf("MailgunDriver options requires a 'secret' address argument") 48 | } 49 | 50 | // Attempt connection to mailgun 51 | mg := mailgun.NewMailgun(domain, APISecret, APIKey) 52 | 53 | return &MailgunDriver{ 54 | domain: domain, 55 | from: address, 56 | key: APIKey, 57 | secret: APISecret, 58 | mg: mg, 59 | testMode: false, 60 | }, nil 61 | } 62 | 63 | func (md *MailgunDriver) Send(address, subject, body string) error { 64 | // Build mailgun message 65 | message := md.mg.NewMessage(md.from, subject, "", address) 66 | message.SetTracking(true) 67 | message.SetHtml(body) 68 | 69 | // Enable test mode if set 70 | if md.testMode == true { 71 | message.EnableTestMode() 72 | } 73 | 74 | // Attempt to send message 75 | _, _, err := md.mg.Send(message) 76 | if err != nil { 77 | log.Printf("MailgunDriver.Send error: %s", err) 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (md *MailgunDriver) SetTestMode(m bool) { 85 | md.testMode = m 86 | } 87 | -------------------------------------------------------------------------------- /lib/controllers/mailer/drivers/mailgun_test.go: -------------------------------------------------------------------------------- 1 | /* AuthPlz Authentication and Authorization Microservice 2 | * Mailgun mailer tests 3 | * 4 | * Copyright 2018 Ryan Kurte 5 | */ 6 | package drivers 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | func TestMailController(t *testing.T) { 15 | 16 | options := make(map[string]string) 17 | 18 | // Fetch options from environment 19 | options["domain"] = os.Getenv("AUTHPLZ_MG_DOMAIN") 20 | options["address"] = os.Getenv("AUTHPLZ_MG_ADDRESS") 21 | options["key"] = os.Getenv("AUTHPLZ_MG_APIKEY") 22 | options["secret"] = os.Getenv("AUTHPLZ_MG_PRIKEY") 23 | 24 | // Skip tests if domain is not valid 25 | if v, ok := options["domain"]; !ok || v == "" { 26 | t.SkipNow() 27 | return 28 | } 29 | 30 | testAddress := "test@kurte.nz" 31 | 32 | // Create driver for test use 33 | d, err := NewMailgunDriver(options) 34 | if err != nil { 35 | t.Error(err) 36 | return 37 | } 38 | 39 | d.SetTestMode(true) 40 | 41 | // Run tests 42 | t.Run("Can send emails", func(t *testing.T) { 43 | err := d.Send(testAddress, "test subject", "test body") 44 | if err != nil { 45 | fmt.Println(err) 46 | t.Error(err) 47 | } 48 | }) 49 | 50 | } 51 | -------------------------------------------------------------------------------- /lib/controllers/mailer/mailer_interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Mailer module controller 3 | * This manages email sending based on system events 4 | * 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package mailer 9 | 10 | import ( 11 | "time" 12 | 13 | "github.com/authplz/authplz-core/lib/api" 14 | ) 15 | 16 | // MailDriver defines the interface that must be implemented by a concrete mailer driver 17 | type MailDriver interface { 18 | Send(to, subject, body string) error 19 | } 20 | 21 | // Storer required by mailer 22 | type Storer interface { 23 | GetUserByExtID(extID string) (interface{}, error) 24 | } 25 | 26 | // User objects returned by storer 27 | type User interface { 28 | GetExtID() string 29 | GetUsername() string 30 | GetEmail() string 31 | } 32 | 33 | // TokenCreator generates action tokens for inclusion in emails 34 | type TokenCreator interface { 35 | BuildToken(userID string, action api.TokenAction, duration time.Duration) (string, error) 36 | } 37 | -------------------------------------------------------------------------------- /lib/controllers/mailer/mailer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Mailer module controller 3 | * This manages email sending based on system events 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package mailer 10 | 11 | import ( 12 | "fmt" 13 | "testing" 14 | "time" 15 | 16 | "github.com/stretchr/testify/assert" 17 | 18 | "github.com/authplz/authplz-core/lib/api" 19 | "github.com/authplz/authplz-core/lib/controllers/datastore" 20 | "github.com/authplz/authplz-core/lib/events" 21 | ) 22 | 23 | type FakeTokenGenerator struct { 24 | } 25 | 26 | func (ftg *FakeTokenGenerator) BuildToken(userID string, action api.TokenAction, duration time.Duration) (string, error) { 27 | return fmt.Sprintf("%s:%s:%s", userID, action, duration), nil 28 | } 29 | 30 | type FakeStorer struct { 31 | Users map[string]datastore.User 32 | } 33 | 34 | func (fs *FakeStorer) GetUserByExtID(extID string) (interface{}, error) { 35 | u, ok := fs.Users[extID] 36 | if !ok { 37 | return nil, fmt.Errorf("User %s not found", extID) 38 | } 39 | return &u, nil 40 | } 41 | 42 | type FakeDriver struct { 43 | To string 44 | Subject string 45 | Body string 46 | } 47 | 48 | func (fd *FakeDriver) Send(to, subject, body string) error { 49 | fd.To = to 50 | fd.Subject = subject 51 | fd.Body = body 52 | return nil 53 | } 54 | 55 | func TestMailController(t *testing.T) { 56 | 57 | var mc *MailController 58 | 59 | options := make(map[string]string) 60 | options["domain"] = "kurte.nz" 61 | options["address"] = "admin@kurte.nz" 62 | 63 | testAddress := "test@kurte.nz" 64 | 65 | storer := FakeStorer{make(map[string]datastore.User)} 66 | storer.Users["test-id"] = datastore.User{ 67 | ExtID: "test-id", 68 | Username: "test-username", 69 | Email: "test-email", 70 | } 71 | 72 | driver := FakeDriver{} 73 | 74 | // Run tests 75 | t.Run("Create mail controller", func(t *testing.T) { 76 | lmc, err := NewMailController("AuthPlz Test", "test.kurte.nz", "logger", options, &storer, &FakeTokenGenerator{}, "../../../templates") 77 | assert.Nil(t, err) 78 | 79 | lmc.driver = &driver 80 | mc = lmc 81 | }) 82 | 83 | t.Run("Can send emails", func(t *testing.T) { 84 | err := mc.SendMail(testAddress, "test subject", "test body") 85 | assert.Nil(t, err) 86 | }) 87 | 88 | t.Run("Can send activation emails", func(t *testing.T) { 89 | data := make(map[string]string) 90 | data["ServiceName"] = mc.appName 91 | data["ActionURL"] = "https://not.a.url/action?token=reset" 92 | data["UserName"] = "TestUser" 93 | 94 | err := mc.SendActivation(testAddress, data) 95 | assert.Nil(t, err) 96 | assert.EqualValues(t, driver.Subject, fmt.Sprintf("%s Account Activation", mc.appName)) 97 | }) 98 | 99 | t.Run("Can send password reset emails", func(t *testing.T) { 100 | data := make(map[string]string) 101 | data["ServiceName"] = mc.appName 102 | data["ActionURL"] = "https://not.a.url/recovery?token=reset" 103 | data["UserName"] = "TestUser" 104 | 105 | err := mc.SendPasswordReset(testAddress, data) 106 | assert.Nil(t, err) 107 | assert.EqualValues(t, driver.Subject, fmt.Sprintf("%s Password Reset", mc.appName)) 108 | }) 109 | 110 | t.Run("Handles AccountCreated event", func(t *testing.T) { 111 | e := events.AuthPlzEvent{ 112 | UserExtID: "test-id", 113 | Time: time.Now(), 114 | Type: events.AccountCreated, 115 | Data: make(map[string]string), 116 | } 117 | 118 | err := mc.HandleEvent(&e) 119 | assert.Nil(t, err) 120 | 121 | assert.EqualValues(t, driver.Subject, fmt.Sprintf("%s Account Activation", mc.appName)) 122 | }) 123 | 124 | t.Run("Handles StartRecovery event", func(t *testing.T) { 125 | e := events.AuthPlzEvent{ 126 | UserExtID: "test-id", 127 | Time: time.Now(), 128 | Type: events.PasswordResetReq, 129 | Data: make(map[string]string), 130 | } 131 | 132 | err := mc.HandleEvent(&e) 133 | assert.Nil(t, err) 134 | 135 | assert.EqualValues(t, driver.Subject, fmt.Sprintf("%s Password Reset", mc.appName)) 136 | }) 137 | 138 | } 139 | -------------------------------------------------------------------------------- /lib/controllers/token/token.go: -------------------------------------------------------------------------------- 1 | // Implements JWT token building and parsing 2 | // This is used for actions such as user activation, login, account unlock. 3 | 4 | package token 5 | 6 | import ( 7 | "encoding/gob" 8 | "fmt" 9 | "log" 10 | "time" 11 | 12 | "github.com/dgrijalva/jwt-go" 13 | "github.com/satori/go.uuid" 14 | 15 | "github.com/authplz/authplz-core/lib/api" 16 | ) 17 | 18 | // Custom claims object 19 | type TokenClaims struct { 20 | Action api.TokenAction `json:"act"` // Token action 21 | jwt.StandardClaims 22 | } 23 | 24 | // TokenController instance 25 | type TokenController struct { 26 | address string 27 | hmacSecret []byte 28 | storer Storer 29 | } 30 | 31 | // Default signing method 32 | var signingMethod jwt.SigningMethod = jwt.SigningMethodHS256 33 | 34 | func init() { 35 | gob.Register(&TokenClaims{}) 36 | } 37 | 38 | //NewTokenController constructor 39 | func NewTokenController(address string, hmacSecret string, storer Storer) *TokenController { 40 | return &TokenController{address: address, hmacSecret: []byte(hmacSecret), storer: storer} 41 | } 42 | 43 | // Generate an action token 44 | func (tc *TokenController) buildSignedToken(userID, tokenID string, action api.TokenAction, duration time.Duration) (string, error) { 45 | 46 | claims := TokenClaims{ 47 | Action: action, 48 | StandardClaims: jwt.StandardClaims{ 49 | Id: tokenID, 50 | IssuedAt: time.Now().Unix(), 51 | ExpiresAt: time.Now().Add(duration).Unix(), 52 | Subject: userID, 53 | Issuer: tc.address, 54 | }, 55 | } 56 | 57 | token := jwt.NewWithClaims(signingMethod, claims) 58 | 59 | // Sign and get the complete encoded token as a string using the secret 60 | tokenString, err := token.SignedString(tc.hmacSecret) 61 | 62 | return tokenString, err 63 | } 64 | 65 | // BuildToken builds a signed token for the given user id with a provided action and duration for use 66 | func (tc *TokenController) BuildToken(userID string, action api.TokenAction, duration time.Duration) (string, error) { 67 | 68 | tokenID := uuid.NewV4().String() 69 | 70 | _, err := tc.storer.CreateActionToken(userID, tokenID, string(action), time.Now().Add(duration)) 71 | if err != nil { 72 | return "", err 73 | } 74 | 75 | signedToken, err := tc.buildSignedToken(userID, tokenID, action, duration) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | return signedToken, nil 81 | } 82 | 83 | // Parse and validate an action token 84 | func (tc *TokenController) parseToken(tokenString string) (*TokenClaims, error) { 85 | 86 | token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) { 87 | // Validate algorithm is correct 88 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 89 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 90 | } 91 | // Return secret 92 | return tc.hmacSecret, nil 93 | }) 94 | 95 | claims, ok := token.Claims.(*TokenClaims) 96 | if ok && token.Valid { 97 | return claims, nil 98 | } else { 99 | fmt.Println(err) 100 | return nil, err 101 | } 102 | } 103 | 104 | // ValidateToken validates a token using the provided key and backing store 105 | func (tc *TokenController) ValidateToken(userID, tokenString string) (*api.TokenAction, error) { 106 | // Parse token 107 | claims, err := tc.parseToken(tokenString) 108 | if err != nil { 109 | log.Printf("TokenController.ValidateToken: Invalid or expired token (%s)", err) 110 | return nil, err 111 | } 112 | 113 | // Check subject matches 114 | if claims.Subject != userID { 115 | log.Println("TokenController.ValidateToken: Subject ID mismatch") 116 | return nil, api.TokenErrorInvalidUser 117 | } 118 | 119 | // Fetch from backing db 120 | t, err := tc.storer.GetActionToken(claims.Id) 121 | if err != nil { 122 | log.Println("TokenController.ValidateToken: No matching token found in datastore") 123 | return nil, api.TokenErrorNotFound 124 | } 125 | token := t.(Token) 126 | 127 | // Check components match 128 | if token.GetUserExtID() != claims.Subject { 129 | log.Println("TokenController.ValidateToken: Token subject mismatch") 130 | return nil, api.TokenErrorInvalidUser 131 | } 132 | if token.GetAction() != string(claims.Action) { 133 | log.Println("TokenController.ValidateToken: Token action mismatch") 134 | return nil, api.TokenErrorInvalidAction 135 | } 136 | if token.IsUsed() { 137 | log.Println("TokenController.ValidateToken: Token already used") 138 | return nil, api.TokenErrorAlreadyUsed 139 | } 140 | 141 | // Return claim 142 | return &claims.Action, nil 143 | } 144 | 145 | // SetUsed marks a token as used in the backing datastore 146 | func (tc *TokenController) SetUsed(tokenString string) error { 147 | // Parse and validate 148 | claims, err := tc.parseToken(tokenString) 149 | if err != nil { 150 | log.Printf("TokenController.ValidateToken: Invalid or expired token (%s)", err) 151 | return err 152 | } 153 | 154 | // Fetch from backing db 155 | t, err := tc.storer.GetActionToken(claims.Id) 156 | if err != nil { 157 | return err 158 | } 159 | token := t.(Token) 160 | 161 | token.SetUsed(time.Now()) 162 | 163 | _, err = tc.storer.UpdateActionToken(token) 164 | 165 | return err 166 | } 167 | -------------------------------------------------------------------------------- /lib/controllers/token/token_interface.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Token defines the interface required for stored tokens 8 | type Token interface { 9 | GetTokenID() string 10 | GetUserExtID() string 11 | GetAction() string 12 | IsUsed() bool 13 | GetExpiry() time.Time 14 | SetUsed(t time.Time) 15 | } 16 | 17 | // Storer defines the backing storage required by the token controller 18 | type Storer interface { 19 | CreateActionToken(userID, tokenID, action string, expiry time.Time) (interface{}, error) 20 | GetActionToken(tokenID string) (interface{}, error) 21 | UpdateActionToken(token interface{}) (interface{}, error) 22 | } 23 | -------------------------------------------------------------------------------- /lib/controllers/token/token_test.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/satori/go.uuid" 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/authplz/authplz-core/lib/api" 12 | "github.com/authplz/authplz-core/lib/controllers/datastore" 13 | ) 14 | 15 | type FakeActionTokenStore struct { 16 | tokens map[string]datastore.ActionToken 17 | } 18 | 19 | func NewFakeActionTokenStore() *FakeActionTokenStore { 20 | return &FakeActionTokenStore{ 21 | tokens: make(map[string]datastore.ActionToken), 22 | } 23 | } 24 | 25 | func (f *FakeActionTokenStore) CreateActionToken(userID, tokenID, action string, expiry time.Time) (interface{}, error) { 26 | t := datastore.ActionToken{ 27 | TokenID: tokenID, 28 | UserExtID: userID, 29 | Action: action, 30 | ExpiresAt: expiry, 31 | Used: false, 32 | } 33 | 34 | f.tokens[tokenID] = t 35 | 36 | return &t, nil 37 | } 38 | 39 | func (f *FakeActionTokenStore) GetActionToken(tokenID string) (interface{}, error) { 40 | t, ok := f.tokens[tokenID] 41 | if !ok { 42 | return nil, fmt.Errorf("No matching token found") 43 | } 44 | return &t, nil 45 | } 46 | 47 | func (f *FakeActionTokenStore) UpdateActionToken(t interface{}) (interface{}, error) { 48 | token := t.(*datastore.ActionToken) 49 | 50 | f.tokens[token.TokenID] = *token 51 | 52 | return token, nil 53 | } 54 | 55 | func TestTokenController(t *testing.T) { 56 | 57 | var fakeHmacKey string = "01234567890123456789012345678901" 58 | var fakeAddress string = "localhost" 59 | 60 | fakeStore := NewFakeActionTokenStore() 61 | 62 | tc := NewTokenController(fakeAddress, fakeHmacKey, fakeStore) 63 | 64 | var fakeUserExtID = uuid.NewV4().String() 65 | var tokenString string 66 | 67 | // Run tests 68 | t.Run("Generates tokens", func(t *testing.T) { 69 | d, _ := time.ParseDuration("10m") 70 | token, err := tc.BuildToken(fakeUserExtID, "activate", d) 71 | assert.Nil(t, err) 72 | if len(token) == 0 { 73 | t.Error("Token creation failed") 74 | } 75 | tokenString = token 76 | }) 77 | 78 | t.Run("Parses tokens", func(t *testing.T) { 79 | claims, err := tc.parseToken(tokenString) 80 | assert.Nil(t, err) 81 | if claims == nil { 82 | t.Error("Token returned no claims") 83 | t.FailNow() 84 | } 85 | fmt.Println(claims) 86 | if (claims.Action != "activate") || (claims.StandardClaims.Subject != fakeUserExtID) { 87 | t.Error("Mismatched claims & subject") 88 | } 89 | }) 90 | 91 | t.Run("Validates tokens", func(t *testing.T) { 92 | action, err := tc.ValidateToken(fakeUserExtID, tokenString) 93 | assert.Nil(t, err) 94 | assert.EqualValues(t, api.TokenActionActivate, *action) 95 | }) 96 | 97 | t.Run("Rejects invalid token signatures", func(t *testing.T) { 98 | brokenToken := []byte(tokenString[0 : len(tokenString)-1]) 99 | brokenToken[len(brokenToken)-1] = brokenToken[len(brokenToken)-1] - 1 100 | brokenString := string(brokenToken) 101 | 102 | _, err := tc.parseToken(brokenString) 103 | if err == nil { 104 | t.Error("Invalid token should cause error") 105 | t.FailNow() 106 | } 107 | }) 108 | 109 | t.Run("Rejects expired tokens", func(t *testing.T) { 110 | d, _ := time.ParseDuration("-10m") 111 | token, err := tc.BuildToken(fakeUserExtID, "activate", d) 112 | assert.Nil(t, err) 113 | 114 | _, err = tc.parseToken(token) 115 | if err == nil { 116 | t.Error("Expired token should cause error") 117 | t.FailNow() 118 | } 119 | }) 120 | 121 | t.Run("Tokens can only be used once", func(t *testing.T) { 122 | _, err := tc.ValidateToken(fakeUserExtID, tokenString) 123 | assert.Nil(t, err, "Expected token validation to succeed") 124 | 125 | tc.SetUsed(tokenString) 126 | 127 | _, err = tc.ValidateToken(fakeUserExtID, tokenString) 128 | assert.EqualValues(t, api.TokenErrorAlreadyUsed, err, "Expected token validation to be blocked") 129 | }) 130 | 131 | } 132 | -------------------------------------------------------------------------------- /lib/events/events.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Events Module 3 | * This defines events for asynchronous communication between modules and plugins 4 | * 5 | * AuthEngine Project (https://github.com/ryankurte/authengine) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package events 10 | 11 | import ( 12 | "time" 13 | ) 14 | 15 | // EventType wraps strings for type safety 16 | type EventType string 17 | 18 | // Account Events 19 | const ( 20 | AccountCreated string = "account_created" 21 | AccountActivated string = "account_activated" 22 | AccountNotActivated string = "account_not_activated" 23 | AccountLocked string = "account_locked" 24 | AccountUnlocked string = "account_unlocked" 25 | AccountNotUnlocked string = "account_not_unlocked" 26 | AccountEnabled string = "account_enabled" 27 | AccountDisabled string = "account_disabled" 28 | AccountNotEnabled string = "account_not_enabled" 29 | AccountDeleted string = "account_deleted" 30 | PasswordUpdate string = "password_update" 31 | PasswordResetReq string = "password_reset_request" 32 | ) 33 | 34 | // 2FA Events 35 | const ( 36 | SecondFactorTotpAdded string = "totp_added" 37 | SecondFactorTotpUsed string = "totp_used" 38 | SecondFactorTotpRemoved string = "totp_removed" 39 | SecondFactorU2FAdded string = "u2f_added" 40 | SecondFactorU2FUsed string = "u2f_used" 41 | SecondFactorU2FRemoved string = "u2f_removed" 42 | SecondFactorBackupCodesAdded string = "backup_code_added" 43 | SecondFactorBackupCodesUsed string = "backup_code_used" 44 | SecondFactorBackupCodesRemoved string = "backup_code_removed" 45 | ) 46 | 47 | // Login Events 48 | const ( 49 | LoginSuccess string = "login_success" 50 | LoginFailure string = "login_failure" 51 | AccountLoginNewDevice string = "login_new_device" 52 | Logout string = "logout" 53 | ) 54 | 55 | // OAuth Events 56 | const ( 57 | OAuthClientCreated string = "oauth_client_created" 58 | OAuthClientRemoved string = "oauth_client_removed" 59 | OAuthClientAuthorized string = "oauth_client_authorized" 60 | OAuthClientDeauthorized string = "oauth_client_deauthorized" 61 | ) 62 | 63 | // AuthPlzEvent event type for asynchronous communication 64 | type AuthPlzEvent struct { 65 | UserExtID string 66 | Time time.Time 67 | Type string 68 | Data map[string]string 69 | } 70 | 71 | // GetType fetches the event type 72 | func (e *AuthPlzEvent) GetType() string { return e.Type } 73 | 74 | // GetUserExtID fetches the associated external user id 75 | func (e *AuthPlzEvent) GetUserExtID() string { return e.UserExtID } 76 | 77 | // GetTime fetches the event originator time 78 | func (e *AuthPlzEvent) GetTime() time.Time { return e.Time } 79 | 80 | // GetData fetches data associated with the event 81 | func (e *AuthPlzEvent) GetData() map[string]string { return e.Data } 82 | 83 | // NewEvent Create a new AuthPlz event 84 | func NewEvent(userExtID, eventType string, data map[string]string) *AuthPlzEvent { 85 | return &AuthPlzEvent{userExtID, time.Now(), eventType, data} 86 | } 87 | 88 | // NewData creates a new blank data object 89 | func NewData() map[string]string { 90 | return make(map[string]string) 91 | } 92 | 93 | // Emitter interface for event producers 94 | type Emitter interface { 95 | SendEvent(interface{}) 96 | } 97 | -------------------------------------------------------------------------------- /lib/modules/2fa/backup/backup_api.go: -------------------------------------------------------------------------------- 1 | /* 2 | * (2fa) Backup Code Module API 3 | * This defines the API methods bound to the 2fa Backup Code module 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package backup 10 | 11 | import ( 12 | "log" 13 | "net/http" 14 | 15 | "github.com/authplz/authplz-core/lib/api" 16 | "github.com/authplz/authplz-core/lib/appcontext" 17 | "github.com/gocraft/web" 18 | "github.com/gorilla/sessions" 19 | ) 20 | 21 | // U2F API context storage 22 | type backupCodeAPICtx struct { 23 | // Base context for shared components 24 | *appcontext.AuthPlzCtx 25 | 26 | // backupCode controller module 27 | backupCodeModule *Controller 28 | 29 | // backupCode session 30 | backupCodeSession *sessions.Session 31 | } 32 | 33 | // Helper middleware to bind module to API context 34 | func bindBackupCodeContext(backupCodeModule *Controller) func(ctx *backupCodeAPICtx, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 35 | return func(ctx *backupCodeAPICtx, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 36 | ctx.backupCodeModule = backupCodeModule 37 | next(rw, req) 38 | } 39 | } 40 | 41 | // BindAPI Binds the API for the totp module to the provided router 42 | func (backupCodeModule *Controller) BindAPI(router *web.Router) { 43 | // Create router for user modules 44 | backupCodeRouter := router.Subrouter(backupCodeAPICtx{}, "/api/backupcode") 45 | 46 | // Attach module context 47 | backupCodeRouter.Middleware(bindBackupCodeContext(backupCodeModule)) 48 | 49 | // Bind endpoints 50 | backupCodeRouter.Get("/create", (*backupCodeAPICtx).CreateTokens) 51 | backupCodeRouter.Post("/authenticate", (*backupCodeAPICtx).AuthenticatePost) 52 | backupCodeRouter.Get("/codes", (*backupCodeAPICtx).ListTokens) 53 | backupCodeRouter.Get("/clear", (*backupCodeAPICtx).RemoveTokens) 54 | } 55 | 56 | // CreateTokens creates a set of backup codes and returns them to the user 57 | func (c *backupCodeAPICtx) CreateTokens(rw web.ResponseWriter, req *web.Request) { 58 | // Check if user is logged in 59 | if c.GetUserID() == "" { 60 | c.WriteUnauthorized(rw) 61 | return 62 | } 63 | 64 | overwrite := req.FormValue("overwrite") 65 | 66 | // Check if codes already exist, then decide what to do 67 | supported := c.backupCodeModule.IsSupported(c.GetUserID()) 68 | if supported && overwrite == "" { 69 | // No overwrite flag, return an API error 70 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.BackupTokenOverwriteRequired) 71 | return 72 | } else if supported && overwrite == "true" { 73 | // Overwrite flag, clear pending tokens and continue 74 | err := c.backupCodeModule.ClearPendingTokens(c.GetUserID()) 75 | if err != nil { 76 | log.Printf("BackupCodeApiCtx.CreateTokens: error clearing pending backup codes (%s)", err) 77 | c.WriteInternalError(rw) 78 | return 79 | } 80 | } 81 | 82 | // Create new codes 83 | codes, err := c.backupCodeModule.CreateCodes(c.GetUserID()) 84 | if err != nil { 85 | log.Printf("BackupCodeApiCtx.CreateTokens: error creating backup codes (%s)", err) 86 | c.WriteInternalError(rw) 87 | return 88 | } 89 | 90 | // Return response 91 | c.WriteJSON(rw, codes) 92 | } 93 | 94 | // AuthenticatePost authenticates a backup code 95 | func (c *backupCodeAPICtx) AuthenticatePost(rw web.ResponseWriter, req *web.Request) { 96 | 97 | // Fetch challenge user ID 98 | userid, action := c.Get2FARequest(rw, req) 99 | if userid == "" || action == "" { 100 | log.Printf("BackupCodeAPICtx.AuthenticatePost No pending 2fa requests found") 101 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.SecondFactorNoRequestSession) 102 | return 103 | } 104 | 105 | log.Printf("BackupCodeAPICtx.AuthenticatePost Authentication request for user %s", userid) 106 | 107 | // Fetch challenge code 108 | code := req.FormValue("code") 109 | 110 | ok, err := c.backupCodeModule.ValidateCode(userid, code) 111 | if err != nil { 112 | log.Printf("backupCodeAuthenticatePost: error validating backup code (%s)", err) 113 | c.WriteInternalError(rw) 114 | return 115 | } 116 | 117 | if !ok { 118 | log.Printf("BackupCodeAPICtx.AuthenticatePost: authentication failed for user %s\n", userid) 119 | c.WriteAPIResultWithCode(rw, http.StatusUnauthorized, api.SecondFactorFailed) 120 | return 121 | } 122 | 123 | c.UserAction(userid, action, rw, req) 124 | 125 | c.WriteAPIResult(rw, api.SecondFactorSuccess) 126 | } 127 | 128 | // List backup tokens 129 | func (c *backupCodeAPICtx) ListTokens(rw web.ResponseWriter, req *web.Request) { 130 | // Check if user is logged in 131 | if c.GetUserID() == "" { 132 | c.WriteUnauthorized(rw) 133 | return 134 | } 135 | 136 | // Fetch codes 137 | codes, err := c.backupCodeModule.ListCodes(c.GetUserID()) 138 | if err != nil { 139 | log.Printf("Error fetching backupCode tokens %s", err) 140 | c.WriteInternalError(rw) 141 | return 142 | } 143 | 144 | // Write codes out 145 | c.WriteJSON(rw, codes) 146 | } 147 | 148 | func (c *backupCodeAPICtx) RemoveTokens(rw web.ResponseWriter, req *web.Request) { 149 | err := c.backupCodeModule.ClearPendingTokens(c.GetUserID()) 150 | if err != nil { 151 | log.Printf("BackupCodeAPICtx.RemoveTokens: error clearing pending backup codes (%s)", err) 152 | c.WriteInternalError(rw) 153 | return 154 | } 155 | c.WriteAPIResult(rw, api.BackupTokensRemoved) 156 | } 157 | -------------------------------------------------------------------------------- /lib/modules/2fa/backup/backup_interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * (2fa) Backup Code Interfaces 3 | * This defines the interfaces required by the 2fa Backup Code module 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package backup 10 | 11 | import ( 12 | "time" 13 | ) 14 | 15 | // Code backup code instance interface 16 | // Storer backup code objects must implement this interface 17 | type Code interface { 18 | // Get user friendly name 19 | GetName() string 20 | // Get hashed secret 21 | GetHashedSecret() string 22 | // Check if the token has been used 23 | IsUsed() bool 24 | // Set a token used flag 25 | SetUsed() 26 | // Fetch the used time 27 | GetUsedAt() time.Time 28 | // Fetch creation time 29 | GetCreatedAt() time.Time 30 | } 31 | 32 | // Storer Backup Code store interface 33 | // This must be implemented by a storage module to provide persistence to the module 34 | type Storer interface { 35 | // Fetch a user instance by user id (should be able to remove this) 36 | GetUserByExtID(userid string) (interface{}, error) 37 | // Add a backup code to a given user 38 | AddBackupToken(userid, name, secret string) (interface{}, error) 39 | // Fetch backup codes for a given user 40 | GetBackupTokens(userid string) ([]interface{}, error) 41 | // Fetch a backup code by name for a given user 42 | GetBackupTokenByName(userid, name string) (interface{}, error) 43 | // Update a provided backup code 44 | UpdateBackupToken(code interface{}) (interface{}, error) 45 | // Remove valid backup codes 46 | ClearPendingBackupTokens(userid string) error 47 | } 48 | -------------------------------------------------------------------------------- /lib/modules/2fa/backup/backup_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * (2fa) Backup Code Module tests 3 | * This defines the tests for the 2fa Backup Code module 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package backup 10 | 11 | import ( 12 | "strings" 13 | "testing" 14 | 15 | "github.com/golang/mock/gomock" 16 | "github.com/stretchr/testify/assert" 17 | 18 | "github.com/authplz/authplz-core/lib/test" 19 | ) 20 | 21 | type BackupTest struct { 22 | name string 23 | f func(t *testing.T, bc *Controller) 24 | } 25 | 26 | var tests = []BackupTest{} 27 | 28 | func TestBackupModule(t *testing.T) { 29 | 30 | userID := "1" 31 | var keys = make([]BackupKey, 0) 32 | var codes *CreateResponse 33 | 34 | // Mocks don't work unless ctrl is instantiated in every subtest (with the appropriate t) 35 | // There /has/ to be a way of refactoring this, but, idk what it is :-/ 36 | 37 | t.Run("Create backup token", func(t *testing.T) { 38 | ctrl := gomock.NewController(t) 39 | defer ctrl.Finish() 40 | mockStore := NewMockStorer(ctrl) 41 | bc := NewController("Test Service", mockStore, &test.MockEventEmitter{}) 42 | 43 | code, err := bc.generateCode(recoveryKeyLen) 44 | assert.Nil(t, err) 45 | assert.NotNil(t, code) 46 | }) 47 | 48 | t.Run("Create backup tokens for user", func(t *testing.T) { 49 | var err error 50 | ctrl := gomock.NewController(t) 51 | defer ctrl.Finish() 52 | mockStore := NewMockStorer(ctrl) 53 | bc := NewController("Test Service", mockStore, &test.MockEventEmitter{}) 54 | 55 | mockStore.EXPECT().AddBackupToken(userID, gomock.Any(), gomock.Any()).Times(NumRecoveryKeys).Do(func(userID, name, key string) { 56 | keys = append(keys, BackupKey{userID, name, key}) 57 | }) 58 | 59 | codes, err = bc.CreateCodes(userID) 60 | assert.Nil(t, err) 61 | assert.Len(t, keys, NumRecoveryKeys) 62 | assert.Len(t, codes.Tokens, NumRecoveryKeys) 63 | }) 64 | 65 | t.Run("Validate backup tokens for user", func(t *testing.T) { 66 | ctrl := gomock.NewController(t) 67 | defer ctrl.Finish() 68 | mockStore := NewMockStorer(ctrl) 69 | bc := NewController("Test Service", mockStore, &test.MockEventEmitter{}) 70 | 71 | code := strings.Join([]string{codes.Tokens[0].Name, codes.Tokens[0].Code}, " ") 72 | 73 | mockCode := NewMockCode(ctrl) 74 | mockCode.EXPECT().GetName().Return(codes.Tokens[0].Name) 75 | mockCode.EXPECT().IsUsed().Return(false) 76 | mockCode.EXPECT().GetHashedSecret().Return(keys[0].Hash) 77 | mockCode.EXPECT().SetUsed() 78 | 79 | mockStore.EXPECT().GetBackupTokenByName(userID, codes.Tokens[0].Name).Return(mockCode, nil) 80 | 81 | mockCode.EXPECT().GetName().Return(codes.Tokens[0].Name) 82 | mockStore.EXPECT().UpdateBackupToken(mockCode) 83 | 84 | ok, err := bc.ValidateCode(userID, code) 85 | assert.Nil(t, err) 86 | 87 | if !ok { 88 | t.Errorf("Backup code validation failed (expected success) code: %s", code) 89 | } 90 | }) 91 | 92 | t.Run("Backup codes can only be validated once", func(t *testing.T) { 93 | ctrl := gomock.NewController(t) 94 | defer ctrl.Finish() 95 | mockStore := NewMockStorer(ctrl) 96 | bc := NewController("Test Service", mockStore, &test.MockEventEmitter{}) 97 | 98 | code := strings.Join([]string{codes.Tokens[0].Name, codes.Tokens[0].Code}, " ") 99 | 100 | mockCode := NewMockCode(ctrl) 101 | mockCode.EXPECT().GetName().Return(codes.Tokens[0].Name) 102 | mockCode.EXPECT().IsUsed().Return(true) 103 | 104 | mockStore.EXPECT().GetBackupTokenByName(userID, codes.Tokens[0].Name).Return(mockCode, nil) 105 | 106 | ok, err := bc.ValidateCode(userID, code) 107 | assert.Nil(t, err) 108 | if ok { 109 | t.Errorf("Backup code validation succeeded (expected failure)") 110 | } 111 | }) 112 | 113 | } 114 | -------------------------------------------------------------------------------- /lib/modules/2fa/totp/totp_interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * TOTP Module interfaces 3 | * This defines the interfaces required to use the TOTP module 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package totp 10 | 11 | import ( 12 | "time" 13 | ) 14 | 15 | // TokenInterface Token instance interface 16 | // Storer token objects must implement this interface 17 | type TokenInterface interface { 18 | GetExtID() string 19 | GetName() string 20 | GetSecret() string 21 | GetCounter() uint 22 | SetCounter(uint) 23 | GetLastUsed() time.Time 24 | SetLastUsed(time.Time) 25 | } 26 | 27 | // User interface type 28 | // Storer user objects must implement this interface 29 | type User interface { 30 | GetEmail() string 31 | } 32 | 33 | // Storer Token store interface 34 | // This must be implemented by a storage module to provide persistence to the module 35 | type Storer interface { 36 | // Fetch a user instance by user id (should be able to remove this) 37 | GetUserByExtID(userid string) (interface{}, error) 38 | // Add a totp token to a given user 39 | AddTotpToken(userid, name, secret string, counter uint) (interface{}, error) 40 | // Fetch totp tokens for a given user 41 | GetTotpTokens(userid string) ([]interface{}, error) 42 | // Update a provided totp token 43 | UpdateTotpToken(token interface{}) (interface{}, error) 44 | // Remove a totp token 45 | RemoveTotpToken(token interface{}) error 46 | } 47 | 48 | // CompletedHandler Callback for 2fa signature completion 49 | type CompletedHandler interface { 50 | SecondFactorCompleted(userid, action string) 51 | } 52 | -------------------------------------------------------------------------------- /lib/modules/2fa/totp/totp_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * totp Module tests 3 | * This defines u2f module tests 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package totp 10 | 11 | import ( 12 | "testing" 13 | "time" 14 | 15 | "github.com/authplz/authplz-core/lib/config" 16 | "github.com/authplz/authplz-core/lib/controllers/datastore" 17 | "github.com/authplz/authplz-core/lib/test" 18 | 19 | "github.com/pquerna/otp" 20 | totp "github.com/pquerna/otp/totp" 21 | ) 22 | 23 | func TestTOTPModule(t *testing.T) { 24 | var fakeEmail = "test@abc.com" 25 | var fakePass = "abcDEF123@abcDEF123@" 26 | var fakeName = "user.sdfsfdF" 27 | 28 | c, _ := config.DefaultConfig() 29 | 30 | // Attempt database connection 31 | dataStore, err := datastore.NewDataStore(c.Database) 32 | if err != nil { 33 | t.Error("Error opening database") 34 | t.FailNow() 35 | } 36 | 37 | // Force synchronization 38 | dataStore.ForceSync() 39 | 40 | // Create user for tests 41 | u, err := dataStore.AddUser(fakeEmail, fakeName, fakePass) 42 | if err != nil { 43 | t.Error(err) 44 | t.FailNow() 45 | } 46 | user := u.(*datastore.User) 47 | 48 | var token *otp.Key 49 | mockEventEmitter := test.MockEventEmitter{} 50 | 51 | // Instantiate u2f module 52 | totpModule := NewController("localhost", dataStore, &mockEventEmitter) 53 | 54 | t.Run("Create token", func(t *testing.T) { 55 | to, err := totpModule.CreateToken(user.GetExtID()) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | if to == nil { 60 | t.Errorf("Challenge is nil") 61 | } 62 | 63 | token = to 64 | }) 65 | 66 | t.Run("Register tokens", func(t *testing.T) { 67 | // Generate response 68 | code, err := totp.GenerateCode(token.Secret(), time.Now()) 69 | if err != nil { 70 | t.Error(err) 71 | t.FailNow() 72 | } 73 | 74 | ok, err := totpModule.ValidateRegistration(user.GetExtID(), "test token", token.Secret(), code) 75 | if err != nil { 76 | t.Error(err) 77 | t.FailNow() 78 | } 79 | if !ok { 80 | t.Errorf("Token registration validation failed") 81 | } 82 | }) 83 | 84 | t.Run("List tokens", func(t *testing.T) { 85 | tokens, err := totpModule.ListTokens(user.GetExtID()) 86 | if err != nil { 87 | t.Error(err) 88 | t.FailNow() 89 | } 90 | if len(tokens) != 1 { 91 | t.Errorf("Expected 1 token, receved %d tokens", len(tokens)) 92 | } 93 | 94 | }) 95 | 96 | t.Run("Authenticate using a token", func(t *testing.T) { 97 | code, err := totp.GenerateCode(token.Secret(), time.Now()) 98 | if err != nil { 99 | t.Error(err) 100 | t.FailNow() 101 | } 102 | 103 | ok, err := totpModule.ValidateToken(user.GetExtID(), code) 104 | if err != nil { 105 | t.Error(err) 106 | t.FailNow() 107 | } 108 | if !ok { 109 | t.Errorf("Token validation failed") 110 | } 111 | }) 112 | 113 | } 114 | -------------------------------------------------------------------------------- /lib/modules/2fa/u2f/u2f_interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * U2F / Fido Module API interfaces 3 | * This defines the interfaces required to use the u2f module 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package u2f 10 | 11 | import ( 12 | "time" 13 | ) 14 | 15 | // TokenInterface Token instance interface 16 | // This must be implemented by the token storage implementation 17 | type TokenInterface interface { 18 | GetExtID() string 19 | GetName() string 20 | GetKeyHandle() string 21 | GetPublicKey() string 22 | GetCertificate() string 23 | GetCounter() uint 24 | SetCounter(uint) 25 | GetLastUsed() time.Time 26 | SetLastUsed(time.Time) 27 | } 28 | 29 | // Storer U2F Token store interface 30 | // This must be implemented by a storage module to provide persistence to the module 31 | type Storer interface { 32 | // Fetch a user instance by user id (should be able to remove this) 33 | GetUserByExtID(userid string) (interface{}, error) 34 | // Add a fido token to a given user 35 | AddFidoToken(userid, name, keyHandle, publicKey, certificate string, counter uint) (interface{}, error) 36 | // Fetch fido tokens for a given user 37 | GetFidoTokens(userid string) ([]interface{}, error) 38 | // Update a provided fido token 39 | UpdateFidoToken(token interface{}) (interface{}, error) 40 | // Remove the provided fido token 41 | RemoveFidoToken(token interface{}) error 42 | } 43 | 44 | // CompletedHandler Callback for 2fa signature completion 45 | type CompletedHandler interface { 46 | SecondFactorCompleted(userid, action string) 47 | } 48 | -------------------------------------------------------------------------------- /lib/modules/2fa/u2f/u2f_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * U2F Tests 3 | * This defines the interfaces required to use the TOTP module 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package u2f 10 | 11 | import ( 12 | "testing" 13 | 14 | "github.com/authplz/authplz-core/lib/config" 15 | "github.com/authplz/authplz-core/lib/controllers/datastore" 16 | "github.com/authplz/authplz-core/lib/test" 17 | 18 | "github.com/ryankurte/go-u2f" 19 | ) 20 | 21 | func TestU2FModule(t *testing.T) { 22 | var fakeEmail = "test@abc.com" 23 | var fakePass = "abcDEF123@abcDEF123@" 24 | var fakeName = "user.sdfsfdF" 25 | c, _ := config.DefaultConfig() 26 | 27 | // Attempt database connection 28 | dataStore, err := datastore.NewDataStore(c.Database) 29 | if err != nil { 30 | t.Error("Error opening database") 31 | t.FailNow() 32 | } 33 | 34 | // Force synchronization 35 | dataStore.ForceSync() 36 | 37 | // Create user for tests 38 | u, err := dataStore.AddUser(fakeEmail, fakeName, fakePass) 39 | if err != nil { 40 | t.Error(err) 41 | t.FailNow() 42 | } 43 | user := u.(*datastore.User) 44 | 45 | // Create virtual key for testing 46 | vt, _ := u2f.NewVirtualKey() 47 | mockEventEmitter := test.MockEventEmitter{} 48 | 49 | // Instantiate u2f module 50 | u2fModule := NewController("localhost", dataStore, &mockEventEmitter) 51 | 52 | t.Run("Create challenges", func(t *testing.T) { 53 | c, err := u2fModule.GetChallenge(user.GetExtID()) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | if c == nil { 58 | t.Errorf("Challenge is nil") 59 | } 60 | }) 61 | 62 | t.Run("Register tokens", func(t *testing.T) { 63 | challenge, err := u2fModule.GetChallenge(user.GetExtID()) 64 | if err != nil { 65 | t.Error(err) 66 | t.FailNow() 67 | } 68 | 69 | rr := challenge.RegisterRequest() 70 | 71 | resp, err := vt.HandleRegisterRequest(*rr) 72 | if err != nil { 73 | t.Error(err) 74 | t.FailNow() 75 | } 76 | 77 | ok, err := u2fModule.ValidateRegistration(user.GetExtID(), "test token", challenge, resp) 78 | if err != nil { 79 | t.Error(err) 80 | t.FailNow() 81 | } 82 | if !ok { 83 | t.Errorf("Token registration validation failed") 84 | } 85 | }) 86 | 87 | t.Run("List tokens", func(t *testing.T) { 88 | tokens, err := u2fModule.ListTokens(user.GetExtID()) 89 | if err != nil { 90 | t.Error(err) 91 | t.FailNow() 92 | } 93 | if len(tokens) != 1 { 94 | t.Errorf("Expected 1 token, receved %d tokens", len(tokens)) 95 | } 96 | 97 | }) 98 | 99 | t.Run("Authenticate using a token", func(t *testing.T) { 100 | challenge, err := u2fModule.GetChallenge(user.GetExtID()) 101 | if err != nil { 102 | t.Error(err) 103 | t.FailNow() 104 | } 105 | 106 | rr := challenge.SignRequest() 107 | 108 | resp, err := vt.HandleAuthenticationRequest(*rr) 109 | if err != nil { 110 | t.Error(err) 111 | t.FailNow() 112 | } 113 | 114 | ok, err := u2fModule.ValidateSignature(user.GetExtID(), challenge, resp) 115 | if err != nil { 116 | t.Error(err) 117 | t.FailNow() 118 | } 119 | if !ok { 120 | t.Errorf("Token signature validation failed") 121 | } 122 | }) 123 | 124 | } 125 | -------------------------------------------------------------------------------- /lib/modules/audit/audit.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Audit module controller 3 | * This defines the controller for the audit module 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package audit 10 | 11 | import ( 12 | "log" 13 | "time" 14 | ) 15 | 16 | // Controller instance 17 | type Controller struct { 18 | store Storer 19 | } 20 | 21 | // NewController Instantiates an audit controller 22 | func NewController(store Storer) *Controller { 23 | return &Controller{store: store} 24 | } 25 | 26 | // AddEvent adds an event to the audit log 27 | func (ac *Controller) AddEvent(userExtID, eventType string, eventTime time.Time, data map[string]string) error { 28 | _, err := ac.store.AddAuditEvent(userExtID, eventType, eventTime, data) 29 | if err != nil { 30 | log.Printf("AuditController.AddEvent: error adding audit event (%s)", err) 31 | return err 32 | } 33 | 34 | log.Printf("AuditController.AddEvent: added event %s for user %s", eventType, userExtID) 35 | 36 | return nil 37 | } 38 | 39 | // HandleEvent handles async events for go-async 40 | func (ac *Controller) HandleEvent(event interface{}) error { 41 | auditEvent := event.(Event) 42 | ac.AddEvent(auditEvent.GetUserExtID(), auditEvent.GetType(), auditEvent.GetTime(), auditEvent.GetData()) 43 | return nil 44 | } 45 | 46 | // ListEvents fetches events for the provided userID 47 | func (ac *Controller) ListEvents(userid string) ([]interface{}, error) { 48 | 49 | events, err := ac.store.GetAuditEvents(userid) 50 | if err != nil { 51 | log.Printf("AuditController.AddEvent: error adding audit event (%s)", err) 52 | return events, err 53 | } 54 | 55 | return events, err 56 | } 57 | -------------------------------------------------------------------------------- /lib/modules/audit/audit_api.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Audit module API 3 | * This defines the API endpoints exposed by the audit module 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package audit 10 | 11 | import ( 12 | "log" 13 | ) 14 | 15 | import ( 16 | "github.com/authplz/authplz-core/lib/appcontext" 17 | "github.com/gocraft/web" 18 | ) 19 | 20 | // APICtx API context instance 21 | type APICtx struct { 22 | // Base context required by router 23 | *appcontext.AuthPlzCtx 24 | // User module instance 25 | ac *Controller 26 | } 27 | 28 | // BindAuditContext Helper middleware to bind module to API context 29 | func BindAuditContext(ac *Controller) func(ctx *APICtx, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 30 | return func(ctx *APICtx, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 31 | ctx.ac = ac 32 | next(rw, req) 33 | } 34 | } 35 | 36 | // BindAPI binds the AuditController API to a provided router 37 | func (ac *Controller) BindAPI(router *web.Router) { 38 | // Create router for user modules 39 | auditRouter := router.Subrouter(APICtx{}, "/api/audit") 40 | 41 | // Attach module context 42 | auditRouter.Middleware(BindAuditContext(ac)) 43 | 44 | // Bind endpoints 45 | auditRouter.Get("/", (*APICtx).GetEvents) 46 | } 47 | 48 | // GetEvents endpoint fetches a list of audit events for a given user 49 | func (c *APICtx) GetEvents(rw web.ResponseWriter, req *web.Request) { 50 | // Check user is logged in 51 | if c.GetUserID() == "" { 52 | c.WriteUnauthorized(rw) 53 | return 54 | } 55 | 56 | events, err := c.ac.ListEvents(c.GetUserID()) 57 | if err != nil { 58 | log.Printf("AuditApiCtx.GetEvents: error listing events (%s)", err) 59 | c.WriteInternalError(rw) 60 | return 61 | } 62 | 63 | c.WriteJSON(rw, events) 64 | } 65 | -------------------------------------------------------------------------------- /lib/modules/audit/audit_interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Audit module interfaces 3 | * This defines the interfaces required by the audit module 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package audit 10 | 11 | import ( 12 | "time" 13 | ) 14 | 15 | // Event Audit event type interface 16 | type Event interface { 17 | GetUserExtID() string 18 | GetType() string 19 | GetTime() time.Time 20 | GetData() map[string]string 21 | } 22 | 23 | // User Audit user type interface 24 | type User interface { 25 | GetExtID() string 26 | } 27 | 28 | // Storer Interface that datastore must implement to provide audit controller 29 | type Storer interface { 30 | AddAuditEvent(userid, eventType string, eventTime time.Time, data map[string]string) (interface{}, error) 31 | GetAuditEvents(userid string) ([]interface{}, error) 32 | } 33 | -------------------------------------------------------------------------------- /lib/modules/audit/audit_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Audit module tests 3 | * This defines tests for the audit module and API 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package audit 10 | 11 | import ( 12 | "testing" 13 | "time" 14 | ) 15 | 16 | import ( 17 | "github.com/authplz/authplz-core/lib/config" 18 | "github.com/authplz/authplz-core/lib/controllers/datastore" 19 | "github.com/authplz/authplz-core/lib/events" 20 | "github.com/ryankurte/go-async" 21 | ) 22 | 23 | func TestAuditController(t *testing.T) { 24 | // Setup user controller for testing 25 | var fakeEmail = "test@abc.com" 26 | var fakePass = "abcDEF123@" 27 | var fakeName = "user.sdfsfdF" 28 | 29 | c, _ := config.DefaultConfig() 30 | 31 | serviceManager := async.NewServiceManager(64) 32 | 33 | // Attempt database connection 34 | ds, err := datastore.NewDataStore(c.Database) 35 | if err != nil { 36 | t.Error("Error opening database") 37 | t.FailNow() 38 | } 39 | ds.ForceSync() 40 | 41 | // Create controllers 42 | ac := NewController(ds) 43 | auditSvc := async.NewAsyncService(ac, 64) 44 | serviceManager.BindService(&auditSvc) 45 | 46 | // Create fake user 47 | u, _ := ds.AddUser(fakeEmail, fakeName, fakePass) 48 | user := u.(*datastore.User) 49 | 50 | // Run tests 51 | t.Run("Add login event", func(t *testing.T) { 52 | err := ac.AddEvent(user.GetExtID(), events.AccountCreated, time.Now(), make(map[string]string)) 53 | if err != nil { 54 | t.Error(err) 55 | } 56 | }) 57 | 58 | t.Run("Can list events", func(t *testing.T) { 59 | events, err := ac.ListEvents(user.GetExtID()) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | if len(events) != 1 { 64 | t.Errorf("Expected 1 event, received %d events", len(events)) 65 | } 66 | }) 67 | 68 | t.Run("Start async server", func(t *testing.T) { 69 | serviceManager.Run() 70 | }) 71 | 72 | t.Run("Post audit event", func(t *testing.T) { 73 | d := make(map[string]string) 74 | d["ip"] = "127.0.0.1" 75 | e := events.AuthPlzEvent{user.GetExtID(), time.Now(), events.AccountActivated, d} 76 | 77 | serviceManager.SendEvent(&e) 78 | 79 | time.Sleep(100 * time.Millisecond) 80 | }) 81 | 82 | t.Run("Can list async events", func(t *testing.T) { 83 | events, err := ac.ListEvents(user.GetExtID()) 84 | if err != nil { 85 | t.Error(err) 86 | } 87 | if len(events) != 2 { 88 | t.Errorf("Expected 2 events, received %d events", len(events)) 89 | } 90 | }) 91 | 92 | t.Run("Stop async server", func(t *testing.T) { 93 | serviceManager.Exit() 94 | }) 95 | 96 | // Tear down user controller 97 | 98 | } 99 | -------------------------------------------------------------------------------- /lib/modules/core/core.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Core module controller 3 | * The core module exposes the base login/logout/reset/action APIs and calls bound handlers to execute each action. 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package core 10 | 11 | import ( 12 | "github.com/authplz/authplz-core/lib/api" 13 | "github.com/authplz/authplz-core/lib/events" 14 | ) 15 | 16 | // Controller core module instance storage 17 | // The core module implements basic login/logout methods and allows binding of modules 18 | // To interrupt/assist/log the execution of each 19 | type Controller struct { 20 | // Token controller for parsing of tokens 21 | tokenControl TokenValidator 22 | 23 | // User controller interface for basic user logins 24 | userControl LoginProvider 25 | 26 | // Token handler implementations 27 | // This allows token handlers to be bound on a per-module basis using the actions 28 | // defined in api.TokenAction. Note that there must not be overlaps in bindings 29 | // TODO: this should probably be implemented as a bind function to panic if overlap is attempted 30 | tokenHandlers map[api.TokenAction]TokenHandler 31 | 32 | // 2nd Factor Authentication implementations 33 | secondFactorHandlers map[string]SecondFactorProvider 34 | 35 | // Event handler implementations 36 | eventHandlers map[string]EventHandler 37 | 38 | // Login handler implementations 39 | preLogin map[string]PreLoginHook 40 | postLoginSuccess map[string]PostLoginSuccessHook 41 | postLoginFailure map[string]PostLoginFailureHook 42 | 43 | // Event emitter for core user states 44 | emitter events.Emitter 45 | } 46 | 47 | // NewController Create a new core module instance 48 | func NewController(tokenValidator TokenValidator, loginProvider LoginProvider, emitter events.Emitter) *Controller { 49 | return &Controller{ 50 | tokenControl: tokenValidator, 51 | userControl: loginProvider, 52 | tokenHandlers: make(map[api.TokenAction]TokenHandler), 53 | secondFactorHandlers: make(map[string]SecondFactorProvider), 54 | 55 | preLogin: make(map[string]PreLoginHook), 56 | postLoginSuccess: make(map[string]PostLoginSuccessHook), 57 | postLoginFailure: make(map[string]PostLoginFailureHook), 58 | eventHandlers: make(map[string]EventHandler), 59 | emitter: emitter, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/modules/core/core_api_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Core API tests 3 | * This tests the Core API endpoints 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | package core 9 | 10 | import ( 11 | "net/http" 12 | "net/url" 13 | "testing" 14 | "time" 15 | 16 | "github.com/stretchr/testify/assert" 17 | 18 | "github.com/authplz/authplz-core/lib/api" 19 | "github.com/authplz/authplz-core/lib/controllers/datastore" 20 | "github.com/authplz/authplz-core/lib/events" 21 | "github.com/authplz/authplz-core/lib/modules/user" 22 | "github.com/authplz/authplz-core/lib/test" 23 | ) 24 | 25 | func TestCoreAPI(t *testing.T) { 26 | 27 | ts, err := test.NewTestServer() 28 | assert.Nil(t, err) 29 | 30 | userModule := user.NewController(ts.DataStore, ts.EventEmitter) 31 | 32 | coreModule := NewController(ts.TokenControl, userModule, ts.EventEmitter) 33 | coreModule.BindModule("user", userModule) 34 | coreModule.BindAPI(ts.Router) 35 | userModule.BindAPI(ts.Router) 36 | 37 | ts.Run() 38 | 39 | v := url.Values{} 40 | v.Set("email", test.FakeEmail) 41 | v.Set("password", test.FakePass) 42 | v.Set("username", test.FakeName) 43 | 44 | client := test.NewClient("http://" + ts.Address() + "/api") 45 | 46 | if _, err := client.PostForm("/create", http.StatusOK, v); err != nil { 47 | t.Error(err) 48 | t.FailNow() 49 | } 50 | 51 | u, _ := ts.DataStore.GetUserByEmail(test.FakeEmail) 52 | 53 | // Activate user and create admin credentals 54 | user := u.(*datastore.User) 55 | user.SetActivated(true) 56 | user.SetAdmin(true) 57 | ts.DataStore.UpdateUser(user) 58 | 59 | t.Run("Login user", func(t *testing.T) { 60 | v := url.Values{} 61 | v.Set("email", test.FakeEmail) 62 | v.Set("password", test.FakePass) 63 | 64 | client := test.NewClient("http://" + ts.Address() + "/api") 65 | 66 | // Attempt login 67 | _, err := client.PostForm("/login", http.StatusOK, v) 68 | assert.Nil(t, err) 69 | 70 | // Check user status 71 | _, err = client.Get("/status", http.StatusOK) 72 | assert.Nil(t, err) 73 | }) 74 | 75 | t.Run("Invalid account fails", func(t *testing.T) { 76 | v := url.Values{} 77 | v.Set("email", "wrong@email.com") 78 | v.Set("password", test.FakePass) 79 | 80 | client := test.NewClient("http://" + ts.Address() + "/api") 81 | 82 | // Attempt login 83 | _, err := client.PostForm("/login", http.StatusUnauthorized, v) 84 | assert.Nil(t, err) 85 | }) 86 | 87 | t.Run("Invalid password fails", func(t *testing.T) { 88 | v := url.Values{} 89 | v.Set("email", test.FakeEmail) 90 | v.Set("password", "Wrong password") 91 | 92 | client := test.NewClient("http://" + ts.Address() + "/api") 93 | 94 | // Attempt login 95 | if _, err := client.PostForm("/login", http.StatusUnauthorized, v); err != nil { 96 | t.Error(err) 97 | t.FailNow() 98 | } 99 | }) 100 | 101 | t.Run("Account recovery endpoints work", func(t *testing.T) { 102 | client := test.NewClient("http://" + ts.Address() + "/api") 103 | 104 | // First, post recovery request to /api/recovery 105 | v := url.Values{} 106 | v.Set("email", test.FakeEmail) 107 | _, err := client.PostForm("/recovery", http.StatusOK, v) 108 | assert.Nil(t, err) 109 | 110 | // Check for recovery event 111 | assert.EqualValues(t, events.PasswordResetReq, ts.EventEmitter.Event.GetType()) 112 | 113 | // Generate a recovery token 114 | d, _ := time.ParseDuration("10m") 115 | token, _ := ts.TokenControl.BuildToken(user.GetExtID(), api.TokenActionRecovery, d) 116 | 117 | // Get recovery endpoint with token 118 | v = url.Values{} 119 | v.Set("token", token) 120 | _, err = client.GetWithParams("/recovery", http.StatusOK, v) 121 | assert.Nil(t, err) 122 | 123 | // Post new password to user reset endpoint 124 | newPass := "Reset Password 78@" 125 | v = url.Values{} 126 | v.Set("password", newPass) 127 | _, err = client.PostForm("/reset", http.StatusOK, v) 128 | assert.Nil(t, err) 129 | }) 130 | 131 | } 132 | -------------------------------------------------------------------------------- /lib/modules/core/core_interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Core module interfaces 3 | * Interfaces used by the core module controller 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package core 10 | 11 | import ( 12 | "github.com/authplz/authplz-core/lib/api" 13 | ) 14 | 15 | // LoginProvider Interface for a user control module 16 | type LoginProvider interface { 17 | // Login method, returns boolean result, user interface for further use, error in case of failure 18 | Login(email string, password string) (bool, interface{}, error) 19 | GetUserByEmail(email string) (interface{}, error) 20 | } 21 | 22 | // TokenValidator Interface for token validation 23 | type TokenValidator interface { 24 | ValidateToken(userid string, tokenString string) (*api.TokenAction, error) 25 | } 26 | 27 | // SecondFactorProvider for 2 factor authentication modules 28 | // These modules must inform the login handler as to whether 29 | // further authentication is supported 30 | type SecondFactorProvider interface { 31 | // Check whether a user can use this 2fa module 32 | // This depends on what second factors are registered 33 | IsSupported(userid string) bool 34 | } 35 | 36 | // TokenHandler for token handler modules 37 | // These modules accept a token action and user id to execute a task 38 | // For example, the user module accepts 'activate' and 'unlock' actions 39 | type TokenHandler interface { 40 | HandleToken(userid string, tokenAction api.TokenAction) error 41 | } 42 | 43 | // Core Event Hook Interfaces 44 | 45 | // PreLoginHook PreLogin hooks may allow or deny login 46 | type PreLoginHook interface { 47 | PreLogin(u interface{}) (bool, error) 48 | } 49 | 50 | // PostLoginSuccessHook Post login success hooks called on login success 51 | type PostLoginSuccessHook interface { 52 | PostLoginSuccess(u interface{}) error 53 | } 54 | 55 | // PostLoginFailureHook Post login failure hooks called on login failure 56 | type PostLoginFailureHook interface { 57 | PostLoginFailure(u interface{}) error 58 | } 59 | 60 | // EventHandler Interface for event handler modules 61 | // These modules are bound into the event manager to provide asynchronous services 62 | // based on system events. 63 | // For example, the mailer module accepts a variety of user events and sends mail in response. 64 | type EventHandler interface { 65 | HandleEvent(userid string, u interface{}) error 66 | } 67 | 68 | // UserInterface Interface for user instances 69 | type UserInterface interface { 70 | GetExtID() string 71 | GetEmail() string 72 | } 73 | -------------------------------------------------------------------------------- /lib/modules/core/core_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Core module controller tests 3 | * Tests the functionality of the core controller 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | package core 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | "time" 14 | 15 | "github.com/authplz/authplz-core/lib/api" 16 | "github.com/authplz/authplz-core/lib/controllers/datastore" 17 | "github.com/authplz/authplz-core/lib/controllers/token" 18 | 19 | "github.com/authplz/authplz-core/lib/test" 20 | ) 21 | 22 | const ( 23 | fakeEmail string = "test@email.com" 24 | fakePass string = "password123@" 25 | ) 26 | 27 | type MockHandler struct { 28 | LoginCallResp bool 29 | SecondFactorRequired bool 30 | TokenAction api.TokenAction 31 | LoginAllowed bool 32 | u interface{} 33 | } 34 | 35 | // user controller interface 36 | func (mh *MockHandler) Login(email string, password string) (bool, interface{}, error) { 37 | var u interface{} 38 | return mh.LoginCallResp, u, nil 39 | } 40 | 41 | func (mh *MockHandler) GetUserByEmail(email string) (interface{}, error) { 42 | return mh.u, nil 43 | } 44 | 45 | // 2fa handler interface 46 | func (mh *MockHandler) IsSupported(userid string) bool { 47 | return mh.SecondFactorRequired 48 | } 49 | 50 | // token handler interface 51 | func (mh *MockHandler) HandleToken(userid string, tokenAction api.TokenAction) error { 52 | mh.TokenAction = tokenAction 53 | return nil 54 | } 55 | 56 | func (mh *MockHandler) PreLogin(u interface{}) (bool, error) { 57 | return mh.LoginAllowed, nil 58 | } 59 | 60 | type FakeActionTokenStore struct { 61 | tokens map[string]datastore.ActionToken 62 | } 63 | 64 | func NewFakeActionTokenStore() *FakeActionTokenStore { 65 | return &FakeActionTokenStore{ 66 | tokens: make(map[string]datastore.ActionToken), 67 | } 68 | } 69 | 70 | func (f *FakeActionTokenStore) CreateActionToken(userID, tokenID, action string, expiry time.Time) (interface{}, error) { 71 | t := datastore.ActionToken{ 72 | TokenID: tokenID, 73 | UserExtID: userID, 74 | Action: action, 75 | ExpiresAt: expiry, 76 | Used: false, 77 | } 78 | 79 | f.tokens[tokenID] = t 80 | 81 | return &t, nil 82 | } 83 | 84 | func (f *FakeActionTokenStore) GetActionToken(tokenID string) (interface{}, error) { 85 | t, ok := f.tokens[tokenID] 86 | if !ok { 87 | return nil, fmt.Errorf("No matching token found") 88 | } 89 | return &t, nil 90 | } 91 | 92 | func (f *FakeActionTokenStore) UpdateActionToken(t interface{}) (interface{}, error) { 93 | token := t.(*datastore.ActionToken) 94 | 95 | f.tokens[token.TokenID] = *token 96 | 97 | return token, nil 98 | } 99 | 100 | func TestCoreModule(t *testing.T) { 101 | 102 | tokenControl := token.NewTokenController("localhost", "ABCD", NewFakeActionTokenStore()) 103 | 104 | mockHandler := MockHandler{false, false, api.TokenActionInvalid, false, nil} 105 | 106 | coreControl := NewController(tokenControl, &mockHandler, &test.MockEventEmitter{}) 107 | 108 | t.Run("Bind and call token action handlers", func(t *testing.T) { 109 | var u interface{} 110 | var mockAction api.TokenAction = "mock-action" 111 | 112 | coreControl.BindActionHandler("mock-action", &mockHandler) 113 | 114 | d, _ := time.ParseDuration("10m") 115 | token, _ := tokenControl.BuildToken("fakeid", mockAction, d) 116 | 117 | mockHandler.TokenAction = api.TokenActionInvalid 118 | ok, err := coreControl.HandleToken("fakeid", u, token) 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | if !ok { 123 | t.Errorf("Token validation failed") 124 | } 125 | if mockHandler.TokenAction != mockAction { 126 | t.Errorf("Action handler not called (expected %+v received %+v)", mockAction, mockHandler.TokenAction) 127 | } 128 | }) 129 | 130 | t.Run("Bind and check second factor handlers", func(t *testing.T) { 131 | coreControl.BindSecondFactor("mock-2fa", &mockHandler) 132 | 133 | mockHandler.SecondFactorRequired = false 134 | required, available := coreControl.CheckSecondFactors("fake") 135 | if required { 136 | t.Errorf("CheckSecondFactors expected required=false, received required=true") 137 | } 138 | if v, ok := available["mock-2fa"]; !ok || v { 139 | t.Errorf("Expected ok, v=false, received %b v=%b", v, ok) 140 | } 141 | 142 | mockHandler.SecondFactorRequired = true 143 | required, available = coreControl.CheckSecondFactors("fake") 144 | if !required { 145 | t.Errorf("CheckSecondFactors expected required=true, received required=false") 146 | } 147 | 148 | if v, ok := available["mock-2fa"]; !ok || !v { 149 | t.Errorf("Expected ok, v=true, received %b v=%b", v, ok) 150 | } 151 | }) 152 | 153 | t.Run("Bind PreLogin handlers", func(t *testing.T) { 154 | var u interface{} 155 | 156 | coreControl.BindPreLogin("mock-login-handler", &mockHandler) 157 | 158 | mockHandler.LoginAllowed = false 159 | ok, err := coreControl.PreLogin(u) 160 | if err != nil { 161 | t.Error(err) 162 | } 163 | if ok { 164 | t.Errorf("Expected login failure") 165 | } 166 | 167 | mockHandler.LoginAllowed = true 168 | ok, err = coreControl.PreLogin(u) 169 | if err != nil { 170 | t.Error(err) 171 | } 172 | if !ok { 173 | t.Errorf("Expected login success") 174 | } 175 | 176 | }) 177 | 178 | t.Run("Bind event handlers", func(t *testing.T) { 179 | 180 | }) 181 | 182 | } 183 | -------------------------------------------------------------------------------- /lib/modules/core/handlers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Core module controller handler management 3 | * 4 | * AuthPlz Project (https://github.com/authplz/authplz-core) 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package core 9 | 10 | import ( 11 | "fmt" 12 | "github.com/authplz/authplz-core/lib/events" 13 | "log" 14 | 15 | "github.com/authplz/authplz-core/lib/api" 16 | ) 17 | 18 | // SecondFactorCompleted handles completion of a 2fa provider 19 | func (coreModule *Controller) SecondFactorCompleted(userid, action string) { 20 | log.Printf("CoreModule.SecondFactorCompleted for user %s action %s", userid, action) 21 | } 22 | 23 | // CheckSecondFactors Determine whether a second factor is required for a user 24 | // This returns a bool indicating whether 2fa is required, and a map of the available 2fa mechanisms 25 | func (coreModule *Controller) CheckSecondFactors(userid string) (bool, map[string]bool) { 26 | availableHandlers := make(map[string]bool) 27 | secondFactorRequired := false 28 | 29 | for key, handler := range coreModule.secondFactorHandlers { 30 | supported := handler.IsSupported(userid) 31 | if supported { 32 | secondFactorRequired = true 33 | } 34 | availableHandlers[key] = supported 35 | } 36 | 37 | return secondFactorRequired, availableHandlers 38 | } 39 | 40 | // HandleToken Handles a token string for a given user 41 | // Returns accepted bool and error in case of failure 42 | func (coreModule *Controller) HandleToken(userid string, user interface{}, tokenString string) (bool, error) { 43 | action, err := coreModule.tokenControl.ValidateToken(userid, tokenString) 44 | if err != nil { 45 | log.Printf("CoreModule.Login: token validation failed %s\n", err) 46 | return false, nil 47 | } 48 | 49 | // Locate token handler 50 | tokenHandler, ok := coreModule.tokenHandlers[*action] 51 | if !ok { 52 | log.Printf("CoreModule.HandleToken: no token handler found for action %s\n", *action) 53 | return false, err 54 | } 55 | 56 | // Execute token action 57 | err = tokenHandler.HandleToken(userid, *action) 58 | if err != nil { 59 | log.Printf("CoreModule.HandleToken: token action %s handler error %s\n", *action, err) 60 | return false, err 61 | } 62 | 63 | log.Printf("CoreModule.HandleToken: token action %v executed for user %s\n", *action, userid) 64 | return true, nil 65 | } 66 | 67 | // HandleRecoveryToken handles a password reset or account recovery token 68 | func (coreModule *Controller) HandleRecoveryToken(email string, tokenString string) (bool, interface{}, error) { 69 | 70 | // Load user 71 | u, err := coreModule.userControl.GetUserByEmail(email) 72 | if err != nil { 73 | log.Printf("CoreModule.HandleRecoveryToken: fetching user failed %s\n", err) 74 | return false, nil, nil 75 | } 76 | user := u.(UserInterface) 77 | 78 | // Validate token 79 | action, err := coreModule.tokenControl.ValidateToken(user.GetExtID(), tokenString) 80 | if err != nil { 81 | log.Printf("CoreModule.HandleRecoveryToken: token validation failed %s\n", err) 82 | return false, nil, nil 83 | } 84 | 85 | // Check for correct action 86 | if *action != api.TokenActionRecovery { 87 | return false, nil, nil 88 | } 89 | 90 | return true, u, nil 91 | } 92 | 93 | // PreLogin Runs bound login handlers to accept user logins 94 | func (coreModule *Controller) PreLogin(u interface{}) (bool, error) { 95 | for key, handler := range coreModule.preLogin { 96 | ok, err := handler.PreLogin(u) 97 | if err != nil { 98 | log.Printf("CoreModule.LoginHandlers: error in handler %s (%s)", key, err) 99 | return false, err 100 | } 101 | if !ok { 102 | log.Printf("CoreModule.LoginHandlers: login blocked by handler %s", key) 103 | return false, nil 104 | } 105 | } 106 | 107 | return true, nil 108 | } 109 | 110 | // PostLoginSuccess Runs bound post login success handlers 111 | func (coreModule *Controller) PostLoginSuccess(u interface{}) error { 112 | for key, handler := range coreModule.postLoginSuccess { 113 | err := handler.PostLoginSuccess(u) 114 | if err != nil { 115 | log.Printf("CoreModule.PostLoginSuccess: error in handler %s (%s)", key, err) 116 | return err 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | // PostLoginFailure Runs bound post login failure handlers 123 | func (coreModule *Controller) PostLoginFailure(u interface{}) error { 124 | for key, handler := range coreModule.postLoginFailure { 125 | err := handler.PostLoginFailure(u) 126 | if err != nil { 127 | log.Printf("CoreModule.PostLoginFailure: error in handler %s (%s)", key, err) 128 | return err 129 | } 130 | } 131 | return nil 132 | } 133 | 134 | // PasswordResetStart Starts a password reset session 135 | func (coreModule *Controller) PasswordResetStart(email string, meta map[string]string) error { 136 | u, err := coreModule.userControl.GetUserByEmail(email) 137 | if err != nil { 138 | return err 139 | } 140 | if u == nil { 141 | log.Printf("CoreModule.PasswordResetStart: no matching user found ('%s')", email) 142 | return fmt.Errorf("No matching user found") 143 | } 144 | user := u.(UserInterface) 145 | coreModule.emitter.SendEvent(events.NewEvent(user.GetExtID(), events.PasswordResetReq, meta)) 146 | 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /lib/modules/core/plugins.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Core module controller plugin management 3 | * 4 | * AuthPlz Project (https://github.com/authplz/authplz-core) 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package core 9 | 10 | import ( 11 | "github.com/authplz/authplz-core/lib/api" 12 | ) 13 | 14 | // BindActionHandler Binds a token action handler instance to the core module 15 | // Token actions are validated and executed following successful login 16 | func (coreModule *Controller) BindActionHandler(action api.TokenAction, thi TokenHandler) { 17 | coreModule.tokenHandlers[action] = thi 18 | } 19 | 20 | // BindSecondFactor Binds a 2fa handler instance into the core module 21 | // 2fa handlers must return whether they are available for a given user. 22 | // If any 2fa module returns true, login will be halted, the user alerted (with available options), 23 | // and a 2fa-pending session variable set in the global context for a 2fa implementation to 24 | // pick up 25 | func (coreModule *Controller) BindSecondFactor(name string, sfi SecondFactorProvider) { 26 | coreModule.secondFactorHandlers[name] = sfi 27 | } 28 | 29 | // BindEventHandler Binds an event handler interface into the core module 30 | // Event handlers are called during a variety of evens 31 | func (coreModule *Controller) BindEventHandler(name string, ehi EventHandler) { 32 | coreModule.eventHandlers[name] = ehi 33 | } 34 | 35 | // BindPreLogin Binds a PreLogin handler interface to the core module 36 | // PreLogin handlers are called in the login chain to check login requirements 37 | func (coreModule *Controller) BindPreLogin(name string, lhi PreLoginHook) { 38 | coreModule.preLogin[name] = lhi 39 | } 40 | 41 | // BindPostLoginSuccess binds a PostLoginSuccess handler interface to the core module 42 | // This handler will be called on successful logins 43 | func (coreModule *Controller) BindPostLoginSuccess(name string, plsi PostLoginSuccessHook) { 44 | coreModule.postLoginSuccess[name] = plsi 45 | } 46 | 47 | // BindPostLoginFailure binds a PostLoginFailure handler interface to the core module 48 | // This handler will be called on failed logins 49 | func (coreModule *Controller) BindPostLoginFailure(name string, plfi PostLoginFailureHook) { 50 | coreModule.postLoginFailure[name] = plfi 51 | } 52 | 53 | // BindModule Magic binding function, detects interfaces implemented by a given module 54 | // and binds as appropriate 55 | func (coreModule *Controller) BindModule(name string, mod interface{}) { 56 | if i, ok := mod.(SecondFactorProvider); ok { 57 | coreModule.BindSecondFactor(name, i) 58 | } 59 | if i, ok := mod.(EventHandler); ok { 60 | coreModule.BindEventHandler(name, i) 61 | } 62 | if i, ok := mod.(PreLoginHook); ok { 63 | coreModule.BindPreLogin(name, i) 64 | } 65 | if i, ok := mod.(PostLoginSuccessHook); ok { 66 | coreModule.BindPostLoginSuccess(name, i) 67 | } 68 | if i, ok := mod.(PostLoginFailureHook); ok { 69 | coreModule.BindPostLoginFailure(name, i) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/modules/oauth/helpers.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "errors" 7 | ) 8 | 9 | func generateSecret(len int) (string, error) { 10 | data := make([]byte, len) 11 | n, err := rand.Read(data) 12 | if err != nil { 13 | return "", err 14 | } 15 | if n != len { 16 | return "", errors.New("Config: RNG failed") 17 | } 18 | 19 | return base64.StdEncoding.EncodeToString(data), nil 20 | } 21 | 22 | func arrayContains(arr []string, line string) bool { 23 | for _, l := range arr { 24 | if l == line { 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | -------------------------------------------------------------------------------- /lib/modules/oauth/oauth_interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * OAuth Module Interfaces 3 | * Defines interfaces required by the OAuth module 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | 9 | package oauth 10 | 11 | import ( 12 | "time" 13 | ) 14 | 15 | // User OAuth user interface 16 | type User interface { 17 | GetExtID() string 18 | IsAdmin() bool 19 | } 20 | 21 | // Client OAuth client application interface 22 | type Client interface { 23 | GetID() string 24 | GetName() string 25 | GetSecret() string 26 | GetRedirectURIs() []string 27 | GetUserData() interface{} 28 | GetScopes() []string 29 | GetGrantTypes() []string 30 | GetResponseTypes() []string 31 | IsPublic() bool 32 | GetCreatedAt() time.Time 33 | GetLastUsed() time.Time 34 | SetLastUsed(time.Time) 35 | } 36 | 37 | // SessionBase defines the common interface across all OAuth sessions 38 | type SessionBase interface { 39 | GetClient() interface{} 40 | GetSession() interface{} 41 | SetSession(session interface{}) 42 | 43 | GetRequestID() string 44 | SetRequestID(string) 45 | GetUserID() string 46 | 47 | GetRequestedAt() time.Time 48 | GetExpiresAt() time.Time 49 | 50 | GetRequestedScopes() []string 51 | SetRequestedScopes(scopes []string) 52 | AppendRequestedScope(scope string) 53 | 54 | GetGrantedScopes() []string 55 | GrantScope(scope string) 56 | 57 | Merge(interface{}) 58 | } 59 | 60 | // AuthorizeCodeSession is an OAuth Authorization Code Grant Session 61 | type AuthorizeCodeSession interface { 62 | SessionBase 63 | GetCode() string 64 | } 65 | 66 | // RefreshTokenSession is an OAuth Refresh Token Session 67 | type RefreshTokenSession interface { 68 | SessionBase 69 | GetSignature() string 70 | } 71 | 72 | // AccessTokenSession is an OAuth Access Token Session 73 | type AccessTokenSession interface { 74 | SessionBase 75 | GetSignature() string 76 | } 77 | 78 | // UserSession is user data associated with an OAuth session 79 | type UserSession interface { 80 | GetUserID() string 81 | GetUsername() string 82 | GetSubject() string 83 | 84 | // Get and Set expiry times 85 | SetAccessExpiry(time.Time) 86 | GetAccessExpiry() time.Time 87 | SetRefreshExpiry(time.Time) 88 | GetRefreshExpiry() time.Time 89 | SetAuthorizeExpiry(time.Time) 90 | GetAuthorizeExpiry() time.Time 91 | SetIDExpiry(time.Time) 92 | GetIDExpiry() time.Time 93 | 94 | Clone() interface{} 95 | } 96 | 97 | // Storer OAuth storage interface 98 | // This must be implemented by the underlying storage device 99 | type Storer interface { 100 | // User storage 101 | GetUserByExtID(userid string) (interface{}, error) 102 | 103 | // Client (application) storage 104 | AddClient(userID, clientID, clientName, secret string, scopes, redirects, grantTypes, responseTypes []string, public bool) (interface{}, error) 105 | GetClientByID(clientID string) (interface{}, error) 106 | GetClientsByUserID(userID string) ([]interface{}, error) 107 | UpdateClient(client interface{}) (interface{}, error) 108 | RemoveClientByID(clientID string) error 109 | 110 | // OAuth User Session Storage 111 | 112 | // Authorization code storage 113 | AddAuthorizeCodeSession(userID, clientID, code, requestID string, requestedAt, expiresAt time.Time, scopes, grantedScopes []string) (interface{}, error) 114 | GetAuthorizeCodeSession(code string) (interface{}, error) 115 | GetAuthorizeCodeSessionByRequestID(requestID string) (interface{}, error) 116 | GetAuthorizeCodeSessionsByUserID(userID string) ([]interface{}, error) 117 | RemoveAuthorizeCodeSession(code string) error 118 | 119 | // Access Token storage 120 | AddAccessTokenSession(userID, clientID, signature, requestID string, requestedAt, expiresAt time.Time, 121 | scopes, grantedScopes []string) (interface{}, error) 122 | GetAccessTokenSession(sgnature string) (interface{}, error) 123 | GetClientByAccessTokenSession(token string) (interface{}, error) 124 | GetAccessTokenSessionByRequestID(requestID string) (interface{}, error) 125 | GetAccessTokenSessionsByUserID(userID string) ([]interface{}, error) 126 | RemoveAccessTokenSession(token string) error 127 | 128 | // Refresh token storage 129 | AddRefreshTokenSession(userID, clientID, signature, requestID string, requestedAt, expiresAt time.Time, scopes, grantedScopes []string) (interface{}, error) 130 | GetRefreshTokenBySignature(signature string) (interface{}, error) 131 | GetRefreshTokenSessionByRequestID(requestID string) (interface{}, error) 132 | GetRefreshTokenSessionsByUserID(userID string) ([]interface{}, error) 133 | RemoveRefreshToken(signature string) error 134 | } 135 | -------------------------------------------------------------------------------- /lib/modules/oauth/oauth_test.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | // +build all controller 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/authplz/authplz-core/lib/config" 10 | "github.com/authplz/authplz-core/lib/controllers/datastore" 11 | "github.com/authplz/authplz-core/lib/test" 12 | ) 13 | 14 | func NoTestOauth(t *testing.T) { 15 | 16 | ts, err := test.NewTestServer() 17 | if err != nil { 18 | t.Error(err) 19 | t.FailNow() 20 | } 21 | 22 | config := config.DefaultOAuthConfig() 23 | 24 | oauthModule := NewController(ts.DataStore, config) 25 | if err != nil { 26 | t.Error(err) 27 | t.FailNow() 28 | } 29 | 30 | u, err := ts.DataStore.AddUser(test.FakeEmail, test.FakeName, test.FakePass) 31 | if err != nil { 32 | t.Error(err) 33 | t.FailNow() 34 | } 35 | user := u.(*datastore.User) 36 | 37 | scopes := []string{"public.read", "public.write", "private.read", "private.write"} 38 | redirects := []string{"https://fake-redirect.cows"} 39 | 40 | grants := []string{"client_credentials"} 41 | responses := []string{"code", "token"} 42 | 43 | t.Run("Users can create specified grant types", func(t *testing.T) { 44 | for i, g := range config.AllowedGrants.Admin { 45 | c, err := oauthModule.CreateClient(user.GetExtID(), fmt.Sprintf("client-test-1.%d", i), scopes, redirects, []string{g}, responses, true) 46 | if arrayContains(config.AllowedGrants.User, g) && err != nil { 47 | t.Error(err) 48 | } 49 | if !arrayContains(config.AllowedGrants.User, g) && err == nil { 50 | t.Errorf("Unexpected allowed grant type: %s", g) 51 | } 52 | if err == nil { 53 | oauthModule.RemoveClient(c.ClientID) 54 | } 55 | } 56 | }) 57 | 58 | t.Run("Admins can create all grant types", func(t *testing.T) { 59 | user.SetAdmin(true) 60 | ts.DataStore.UpdateUser(user) 61 | 62 | for i, g := range config.AllowedGrants.Admin { 63 | c, err := oauthModule.CreateClient(user.GetExtID(), fmt.Sprintf("client-test-2.%d", i), scopes, redirects, []string{g}, responses, true) 64 | if err != nil { 65 | t.Error(err) 66 | } else if c == nil { 67 | t.Errorf("Nil client returned") 68 | } 69 | oauthModule.RemoveClient(c.ClientID) 70 | } 71 | 72 | user.SetAdmin(false) 73 | ts.DataStore.UpdateUser(user) 74 | }) 75 | 76 | t.Run("Users can only create valid scopes", func(t *testing.T) { 77 | scopes := []string{"FakeScope"} 78 | c, err := oauthModule.CreateClient(user.GetExtID(), fmt.Sprintf("client-test-3"), scopes, redirects, grants, responses, true) 79 | if err == nil { 80 | t.Errorf("Unexpected allowed scope: %s", scopes) 81 | oauthModule.RemoveClient(c.ClientID) 82 | } 83 | }) 84 | 85 | t.Run("Client names must be unique", func(t *testing.T) { 86 | user.SetAdmin(true) 87 | ts.DataStore.UpdateUser(user) 88 | 89 | _, err := oauthModule.CreateClient(user.GetExtID(), fmt.Sprintf("client-test-4"), scopes, redirects, grants, responses, true) 90 | if err != nil { 91 | t.Errorf("Unexpected error %s", err) 92 | } 93 | _, err = oauthModule.CreateClient(user.GetExtID(), fmt.Sprintf("client-test-4"), scopes, redirects, grants, responses, true) 94 | if err == nil { 95 | t.Errorf("Expected duplicate client error") 96 | } 97 | oauthModule.RemoveClient(fmt.Sprintf("client-test-4")) 98 | 99 | user.SetAdmin(false) 100 | ts.DataStore.UpdateUser(user) 101 | }) 102 | 103 | } 104 | -------------------------------------------------------------------------------- /lib/modules/oauth/session.go: -------------------------------------------------------------------------------- 1 | /* 2 | * OAuth Module Session Definitions 3 | * This session object is used internally to transfer user and expiry information to the storage providers 4 | * 5 | * AuthPlz Project (https://github.com/authplz/authplz-core) 6 | * Copyright 2017 Ryan Kurte 7 | */ 8 | package oauth 9 | 10 | import ( 11 | "bytes" 12 | "encoding/gob" 13 | "time" 14 | ) 15 | 16 | // Session is an OAuth session for module use 17 | // Relevant data is persisted with each grant type object and returned using a similar object 18 | // meeting the UserSession interface from the datastore 19 | type Session struct { 20 | UserID string 21 | Username string 22 | Subject string 23 | AccessExpiry time.Time 24 | RefreshExpiry time.Time 25 | AuthorizeExpiry time.Time 26 | IDExpiry time.Time 27 | } 28 | 29 | // NewSession creates a new default session instance for a given user 30 | func NewSession(userID, username string) *Session { 31 | return &Session{ 32 | UserID: userID, 33 | Username: username, 34 | AccessExpiry: time.Time{}, 35 | RefreshExpiry: time.Time{}, 36 | AuthorizeExpiry: time.Time{}, 37 | IDExpiry: time.Time{}, 38 | } 39 | } 40 | 41 | func (s *Session) GetUserID() string { return s.UserID } 42 | func (s *Session) GetUsername() string { return s.Username } 43 | func (s *Session) GetSubject() string { return s.Subject } 44 | func (s *Session) SetAccessExpiry(t time.Time) { s.AccessExpiry = t } 45 | func (s *Session) GetAccessExpiry() time.Time { return s.AccessExpiry } 46 | func (s *Session) SetRefreshExpiry(t time.Time) { s.RefreshExpiry = t } 47 | func (s *Session) GetRefreshExpiry() time.Time { return s.RefreshExpiry } 48 | func (s *Session) SetAuthorizeExpiry(t time.Time) { s.AuthorizeExpiry = t } 49 | func (s *Session) GetAuthorizeExpiry() time.Time { return s.AuthorizeExpiry } 50 | func (s *Session) SetIDExpiry(t time.Time) { s.IDExpiry = t } 51 | func (s *Session) GetIDExpiry() time.Time { return s.IDExpiry } 52 | 53 | func (s *Session) Clone() interface{} { 54 | clone := Session{} 55 | 56 | var buf bytes.Buffer 57 | enc := gob.NewEncoder(&buf) 58 | dec := gob.NewDecoder(&buf) 59 | _ = enc.Encode(s) 60 | _ = dec.Decode(&clone) 61 | 62 | return &clone 63 | } 64 | -------------------------------------------------------------------------------- /lib/modules/user/user_api.go: -------------------------------------------------------------------------------- 1 | /* 2 | * User API implementation 3 | * 4 | * AuthPlz Project (https://github.com/authplz/authplz-core) 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package user 9 | 10 | import ( 11 | "log" 12 | "net/http" 13 | "regexp" 14 | "strings" 15 | ) 16 | 17 | import ( 18 | "github.com/asaskevich/govalidator" 19 | "github.com/authplz/authplz-core/lib/api" 20 | "github.com/authplz/authplz-core/lib/appcontext" 21 | "github.com/gocraft/web" 22 | ) 23 | 24 | // API context instance 25 | type apiCtx struct { 26 | // Base context required by router 27 | *appcontext.AuthPlzCtx 28 | // User module instance 29 | um *Controller 30 | } 31 | 32 | // BindUserContext Helper middleware to bind module to API context 33 | func BindUserContext(userModule *Controller) func(ctx *apiCtx, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 34 | return func(ctx *apiCtx, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 35 | ctx.um = userModule 36 | next(rw, req) 37 | } 38 | } 39 | 40 | // BindAPI Binds the API for the user module to the provided router 41 | func (userModule *Controller) BindAPI(router *web.Router) { 42 | // Create router for user modules 43 | userRouter := router.Subrouter(apiCtx{}, "/api") 44 | 45 | // Attach module context 46 | userRouter.Middleware(BindUserContext(userModule)) 47 | 48 | // Bind endpoints 49 | userRouter.Get("/status", (*apiCtx).Status) 50 | userRouter.Post("/create", (*apiCtx).Create) 51 | userRouter.Get("/account", (*apiCtx).AccountGet) 52 | userRouter.Post("/account", (*apiCtx).AccountPost) 53 | userRouter.Post("/reset", (*apiCtx).ResetPost) 54 | } 55 | 56 | // Get user login status 57 | func (c *apiCtx) Status(rw web.ResponseWriter, req *web.Request) { 58 | if c.GetUserID() == "" { 59 | c.WriteUnauthorized(rw) 60 | } else { 61 | c.WriteAPIResult(rw, api.LoginSuccessful) 62 | } 63 | } 64 | 65 | var usernameExp = regexp.MustCompile(`([a-z0-9\.]+)`) 66 | 67 | func (c *apiCtx) Create(rw web.ResponseWriter, req *web.Request) { 68 | 69 | log.Printf("Req: %+v", req) 70 | 71 | email := strings.ToLower(req.FormValue("email")) 72 | if !govalidator.IsEmail(email) { 73 | log.Printf("User.Create: missing or invalid email (%s)", email) 74 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.InvalidEmail) 75 | return 76 | } 77 | username := strings.ToLower(req.FormValue("username")) 78 | if !usernameExp.MatchString(username) { 79 | log.Printf("User.Create: missing or invalid username (%s)", username) 80 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.InvalidUsername) 81 | return 82 | } 83 | password := req.FormValue("password") 84 | if password == "" { 85 | log.Printf("User.Create: password parameter required") 86 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.MissingPassword) 87 | return 88 | } 89 | 90 | u, e := c.um.Create(email, username, password) 91 | if e != nil { 92 | log.Printf("User.Create: user creation failed with %s", e) 93 | if e == ErrorDuplicateAccount { 94 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.DuplicateUserAccount) 95 | return 96 | } else if e == ErrorPasswordTooShort || e == ErrorPasswordEntropyTooLow { 97 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.PasswordComplexityTooLow) 98 | return 99 | } 100 | 101 | c.WriteInternalError(rw) 102 | return 103 | } 104 | 105 | if u == nil { 106 | log.Printf("Create: user creation failed") 107 | c.WriteInternalError(rw) 108 | return 109 | } 110 | 111 | log.Println("Create: Create OK") 112 | 113 | c.WriteAPIResult(rw, api.CreateUserSuccess) 114 | } 115 | 116 | // Fetch a user object 117 | func (c *apiCtx) AccountGet(rw web.ResponseWriter, req *web.Request) { 118 | if c.GetUserID() == "" { 119 | c.WriteUnauthorized(rw) 120 | return 121 | 122 | } 123 | // Fetch user from user controller 124 | u, err := c.um.GetUser(c.GetUserID()) 125 | if err != nil { 126 | log.Print(err) 127 | c.WriteInternalError(rw) 128 | return 129 | } 130 | 131 | c.WriteJSON(rw, u) 132 | } 133 | 134 | // Update user object 135 | func (c *apiCtx) AccountPost(rw web.ResponseWriter, req *web.Request) { 136 | if c.GetUserID() == "" { 137 | c.WriteUnauthorized(rw) 138 | return 139 | } 140 | 141 | // Fetch password arguments 142 | oldPass := req.FormValue("old_password") 143 | if oldPass == "" { 144 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.MissingPassword) 145 | return 146 | } 147 | newPass := req.FormValue("new_password") 148 | if newPass == "" { 149 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.MissingPassword) 150 | return 151 | } 152 | 153 | // Update password 154 | _, err := c.um.UpdatePassword(c.GetUserID(), oldPass, newPass) 155 | if err != nil { 156 | log.Print(err) 157 | c.WriteInternalError(rw) 158 | return 159 | } 160 | 161 | c.WriteAPIResult(rw, api.PasswordUpdated) 162 | } 163 | 164 | // ResetPost handles password reset posts 165 | func (c *apiCtx) ResetPost(rw web.ResponseWriter, req *web.Request) { 166 | if c.GetUserID() != "" { 167 | log.Printf("UserModule.ResetPost user already logged in") 168 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.AlreadyAuthenticated) 169 | return 170 | } 171 | 172 | // Fetch user recovery request userid 173 | userid := c.GetRecoveryRequest(rw, req) 174 | if userid == "" { 175 | log.Printf("UserModule.ResetPost no recovery request found") 176 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.NoRecoveryPending) 177 | return 178 | } 179 | 180 | // Fetch new user password 181 | password := req.FormValue("password") 182 | if password == "" { 183 | log.Printf("UserModule.ResetPost missing password") 184 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.MissingPassword) 185 | return 186 | } 187 | 188 | // Update password 189 | _, err := c.um.SetPassword(userid, password) 190 | if err != nil { 191 | if err == ErrorPasswordTooShort || err == ErrorPasswordEntropyTooLow { 192 | c.WriteAPIResultWithCode(rw, http.StatusBadRequest, api.PasswordComplexityTooLow) 193 | return 194 | } 195 | 196 | log.Printf("UserAPI.ResetPost error setting password (%s)", err) 197 | c.WriteInternalError(rw) 198 | return 199 | } 200 | 201 | // Write OK response 202 | c.WriteAPIResult(rw, api.PasswordUpdated) 203 | } 204 | -------------------------------------------------------------------------------- /lib/modules/user/user_api_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * User API tests 3 | * 4 | * AuthPlz Project (https://github.com/authplz/authplz-core) 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package user 9 | 10 | import ( 11 | "log" 12 | "net/http" 13 | "testing" 14 | 15 | "github.com/gocraft/web" 16 | "github.com/gorilla/context" 17 | "github.com/gorilla/sessions" 18 | 19 | "github.com/authplz/authplz-core/lib/api" 20 | "github.com/authplz/authplz-core/lib/appcontext" 21 | "github.com/authplz/authplz-core/lib/config" 22 | "github.com/authplz/authplz-core/lib/controllers/datastore" 23 | "github.com/authplz/authplz-core/lib/test" 24 | ) 25 | 26 | func TestUserApi(t *testing.T) { 27 | // Setup user controller for testing 28 | var address = "localhost:8811" 29 | 30 | c, _ := config.DefaultConfig() 31 | 32 | // Attempt database connection 33 | dataStore, err := datastore.NewDataStore(c.Database) 34 | if err != nil { 35 | t.Error("Error opening database") 36 | t.FailNow() 37 | } 38 | dataStore.ForceSync() 39 | 40 | // Create controllers 41 | sessionStore := sessions.NewCookieStore([]byte("abcDEF123")) 42 | mockEventEmitter := test.MockEventEmitter{} 43 | userModule := NewController(dataStore, &mockEventEmitter) 44 | 45 | ac := appcontext.AuthPlzGlobalCtx{ 46 | SessionStore: sessionStore, 47 | } 48 | 49 | router := web.New(appcontext.AuthPlzCtx{}). 50 | Middleware(appcontext.BindContext(&ac)). 51 | //Middleware(web.LoggerMiddleware). 52 | //Middleware(web.ShowErrorsMiddleware). 53 | Middleware((*appcontext.AuthPlzCtx).SessionMiddleware). 54 | Middleware((*appcontext.AuthPlzCtx).GetIPMiddleware) 55 | 56 | userModule.BindAPI(router) 57 | 58 | handler := context.ClearHandler(router) 59 | 60 | go func() { 61 | err = http.ListenAndServe(address, handler) 62 | if err != nil { 63 | log.Panic(err) 64 | } 65 | }() 66 | 67 | // Setup test helpers 68 | client := test.NewClient("http://" + address + "/api") 69 | 70 | // Run tests 71 | t.Run("Login status", func(t *testing.T) { 72 | if err := client.GetAPIResponse("/status", http.StatusUnauthorized, api.Unauthorized); err != nil { 73 | t.Error(err) 74 | } 75 | }) 76 | 77 | // TODO: move user api tests here 78 | 79 | } 80 | -------------------------------------------------------------------------------- /lib/modules/user/user_errors.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "errors" 4 | 5 | // User control errors 6 | var ( 7 | ErrorPasswordTooShort = errors.New("User Controller: password does not meet complexity requirements") 8 | ErrorPasswordEntropyTooLow = errors.New("User controller: password entropy too low") 9 | ErrorPasswordHashTooShort = errors.New("User Controller: password hash too short") 10 | ErrorFindingUser = errors.New("User Controller: error checking for user account") 11 | ErrorDuplicateAccount = errors.New("User Controller: user account with email or username already exists") 12 | ErrorCreatingUser = errors.New("User Controller: error creating user") 13 | ErrorUserNotFound = errors.New("User Controller: user not found") 14 | ErrorPasswordMismatch = errors.New("User Controller: password mismatch") 15 | ErrorUpdatingUser = errors.New("User Controller: error updating user") 16 | ErrorAddingToken = errors.New("User Controller: error adding token") 17 | ErrorUpdatingToken = errors.New("User Controller: error updating token") 18 | ) 19 | -------------------------------------------------------------------------------- /lib/modules/user/user_interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * User controller interfaces 3 | * 4 | * AuthPlz Project (https://github.com/authplz/authplz-core) 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package user 9 | 10 | import ( 11 | "errors" 12 | "time" 13 | ) 14 | 15 | // User Defines the User object interfaces required by this module 16 | type User interface { 17 | GetExtID() string 18 | GetEmail() string 19 | GetUsername() string 20 | 21 | GetPassword() string 22 | SetPassword(pass string) 23 | GetPasswordChanged() time.Time 24 | 25 | IsActivated() bool 26 | SetActivated(activated bool) 27 | 28 | IsEnabled() bool 29 | SetEnabled(locked bool) 30 | 31 | GetLoginRetries() uint 32 | SetLoginRetries(retries uint) 33 | 34 | GetLastLogin() time.Time 35 | SetLastLogin(t time.Time) 36 | GetCreatedAt() time.Time 37 | 38 | IsLocked() bool 39 | SetLocked(locked bool) 40 | 41 | IsAdmin() bool 42 | } 43 | 44 | // Storer Defines the required store interfaces for the user module 45 | // Returned interfaces must satisfy the User interface requirements 46 | type Storer interface { 47 | AddUser(email, username, pass string) (interface{}, error) 48 | GetUserByExtID(userid string) (interface{}, error) 49 | GetUserByEmail(email string) (interface{}, error) 50 | GetUserByUsername(username string) (interface{}, error) 51 | UpdateUser(user interface{}) (interface{}, error) 52 | } 53 | 54 | /* 55 | // Login status return objects 56 | type LoginStatus struct { 57 | Code uint64 58 | Message string 59 | } 60 | 61 | // User controller status enumerations 62 | const ( 63 | LoginCodeSuccess = iota // Login complete 64 | LoginCodeFailure = iota // Login failed 65 | LoginCodePartial = iota // Further credentials required 66 | LoginCodeLocked = iota // Account locked 67 | LoginCodeUnactivated = iota // Account not yet activated 68 | LoginCodeDisabled = iota // Account disabled 69 | ) 70 | 71 | // Login return object instances 72 | 73 | var LoginSuccess = LoginStatus{LoginCodeSuccess, "Login successful"} 74 | var LoginFailure = LoginStatus{LoginCodeFailure, "Invalid username or password"} 75 | var LoginRequired = LoginStatus{LoginCodeFailure, "Login required"} 76 | var LoginPartial = LoginStatus{LoginCodeFailure, "Second factor required"} 77 | var LoginLocked = LoginStatus{LoginCodeLocked, "User account locked"} 78 | var LoginUnactivated = LoginStatus{LoginCodeUnactivated, "User account not activated"} 79 | var LoginDisabled = LoginStatus{LoginCodeDisabled, "User account disabled"} 80 | */ 81 | var errLogin = errors.New("internal server error") 82 | -------------------------------------------------------------------------------- /lib/plugins/ratelimit/ratelimit.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Rate limiting plugin (WIP) 3 | * 4 | * AuthPlz Project (https://github.com/authplz/authplz-core) 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package ratelimit 9 | 10 | type RateLimitStorageInterface interface { 11 | } 12 | -------------------------------------------------------------------------------- /lib/test/helpers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Test Helpers 3 | * 4 | * AuthPlz Project (https://github.com/authplz/authplz-core) 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package test 9 | 10 | import ( 11 | "fmt" 12 | "log" 13 | "math/rand" 14 | "net/http" 15 | 16 | "github.com/gocraft/web" 17 | "github.com/gorilla/context" 18 | "github.com/gorilla/sessions" 19 | 20 | "github.com/authplz/authplz-core/lib/appcontext" 21 | "github.com/authplz/authplz-core/lib/config" 22 | "github.com/authplz/authplz-core/lib/controllers/datastore" 23 | "github.com/authplz/authplz-core/lib/controllers/token" 24 | ) 25 | 26 | const ( 27 | FakeEmail = "test@abc.com" 28 | FakePass = "V3vRyT!$5qNHt9H1" 29 | NewPass = "*$M^kiD2Nhs8OpkR" 30 | FakeName = "user.sdfsfdF" 31 | ) 32 | 33 | type TestServer struct { 34 | Router *web.Router 35 | DataStore *datastore.DataStore 36 | TokenControl *token.TokenController 37 | EventEmitter *MockEventEmitter 38 | Config *config.AuthPlzConfig 39 | } 40 | 41 | func NewTestServer() (*TestServer, error) { 42 | c := NewConfig() 43 | 44 | ds, err := datastore.NewDataStore(c.Database) 45 | if err != nil { 46 | return nil, err 47 | } 48 | ds.ForceSync() 49 | 50 | sessionStore := sessions.NewCookieStore([]byte("abcDEF123")) 51 | ac := appcontext.AuthPlzGlobalCtx{ 52 | SessionStore: sessionStore, 53 | } 54 | 55 | tokenControl := token.NewTokenController("localhost", "abcDEF123", ds) 56 | 57 | mockEventEmitter := MockEventEmitter{} 58 | 59 | // Create router with base context 60 | router := web.New(appcontext.AuthPlzCtx{}). 61 | Middleware(appcontext.BindContext(&ac)). 62 | Middleware((*appcontext.AuthPlzCtx).SessionMiddleware) 63 | 64 | return &TestServer{router, ds, tokenControl, &mockEventEmitter, c}, nil 65 | } 66 | 67 | func (ts *TestServer) Address() string { 68 | return fmt.Sprintf("%s:%s", ts.Config.Address, ts.Config.Port) 69 | } 70 | 71 | func (ts *TestServer) Run() { 72 | 73 | handler := context.ClearHandler(ts.Router) 74 | go func() { 75 | err := http.ListenAndServe(ts.Address(), handler) 76 | if err != nil { 77 | log.Fatal("ListenAndServe: ", err) 78 | } 79 | }() 80 | } 81 | 82 | // NewConfig generates a test configuration 83 | func NewConfig() *config.AuthPlzConfig { 84 | c, _ := config.DefaultConfig() 85 | 86 | c.Port = fmt.Sprintf("%d", rand.Uint32()%10000+10000) 87 | 88 | c.TLS.Disabled = true 89 | c.ExternalAddress = fmt.Sprintf("http://%s:%s", c.Address, c.Port) 90 | c.AllowedOrigins = []string{c.ExternalAddress, "https://authplz.herokuapp.com"} 91 | c.TemplateDir = "../../templates" 92 | c.Mailer.Driver = "logger" 93 | c.Mailer.Options = map[string]string{"mode": "silent"} 94 | c.DisableWebSecurity = true 95 | 96 | return c 97 | } 98 | -------------------------------------------------------------------------------- /lib/test/mockemitter.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Mock event emitter for test use 3 | * 4 | * AuthPlz Project (https://github.com/authplz/authplz-core) 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package test 9 | 10 | import ( 11 | "github.com/authplz/authplz-core/lib/events" 12 | ) 13 | 14 | type MockEventEmitter struct { 15 | Event *events.AuthPlzEvent 16 | } 17 | 18 | func (m *MockEventEmitter) SendEvent(e interface{}) { 19 | m.Event = e.(*events.AuthPlzEvent) 20 | } 21 | -------------------------------------------------------------------------------- /lib/test/testclient.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Test client for API and integration tests 3 | * 4 | * AuthPlz Project (https://github.com/authplz/authplz-core) 5 | * Copyright 2017 Ryan Kurte 6 | */ 7 | 8 | package test 9 | 10 | import ( 11 | "bytes" 12 | "encoding/json" 13 | "fmt" 14 | "net/http" 15 | "net/http/cookiejar" 16 | "net/url" 17 | 18 | "github.com/authplz/authplz-core/lib/api" 19 | ) 20 | 21 | // Client instance 22 | // Handles cookies as well as API base addresses and provides convenience wrappers to simplify testing 23 | type Client struct { 24 | *http.Client 25 | basePath string 26 | } 27 | 28 | // NewClient Create a new Client instance 29 | func NewClient(path string) Client { 30 | jar, _ := cookiejar.New(nil) 31 | httpClient := &http.Client{ 32 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 33 | return http.ErrUseLastResponse 34 | }, 35 | Jar: jar, 36 | } 37 | return Client{Client: httpClient, basePath: path} 38 | } 39 | 40 | // NewClientFromHttp Create a new Client instance using the provided http.Client 41 | // Useful for OAuth clients 42 | func NewClientFromHttp(path string, client *http.Client) Client { 43 | jar, _ := cookiejar.New(nil) 44 | client.Jar = jar 45 | client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 46 | return http.ErrUseLastResponse 47 | } 48 | return Client{Client: client, basePath: path} 49 | } 50 | 51 | // Get wraps client.Get with status code checks 52 | func (tc *Client) Get(path string, statusCode int) (*http.Response, error) { 53 | queryPath := tc.basePath + path 54 | 55 | req, _ := http.NewRequest("GET", queryPath, nil) 56 | 57 | resp, err := tc.Do(req) 58 | if err != nil { 59 | return resp, err 60 | } 61 | 62 | if resp.StatusCode != statusCode { 63 | return resp, fmt.Errorf("Incorrect status code from '%s' received: '%d' expected: '%d'", path, resp.StatusCode, statusCode) 64 | } 65 | 66 | return resp, err 67 | } 68 | 69 | //GetWithParamsGet wraps client.Get with query parameters and status code checks 70 | func (tc *Client) GetWithParams(path string, statusCode int, v url.Values) (*http.Response, error) { 71 | queryPath := tc.basePath + path 72 | 73 | req, _ := http.NewRequest("GET", queryPath, nil) 74 | 75 | req.URL.RawQuery = v.Encode() 76 | 77 | resp, err := tc.Do(req) 78 | if err != nil { 79 | return resp, err 80 | } 81 | 82 | if resp.StatusCode != statusCode { 83 | return resp, fmt.Errorf("Incorrect status code from '%s' received: '%d 'expected: '%d'", path, resp.StatusCode, statusCode) 84 | } 85 | 86 | return resp, err 87 | } 88 | 89 | // CheckRedirect checks that a given redirect is correct 90 | func CheckRedirect(url string, resp *http.Response) error { 91 | if loc := resp.Header.Get("Location"); loc != url { 92 | return fmt.Errorf("Invalid location header (actual: '%s' expected: '%s'", loc, url) 93 | } 94 | return nil 95 | } 96 | 97 | // ParseJson assists with parsing JSON responses 98 | func ParseJson(resp *http.Response, inst interface{}) error { 99 | defer resp.Body.Close() 100 | err := json.NewDecoder(resp.Body).Decode(&inst) 101 | if err != nil { 102 | return err 103 | } 104 | return nil 105 | } 106 | 107 | func (tc *Client) GetJSON(path string, statusCode int, inst interface{}) error { 108 | resp, err := tc.Get(path, statusCode) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | return ParseJson(resp, inst) 114 | } 115 | 116 | func (tc *Client) GetJSONWithParams(path string, statusCode int, v url.Values, inst interface{}) error { 117 | resp, err := tc.GetWithParams(path, statusCode, v) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | return ParseJson(resp, inst) 123 | } 124 | 125 | // CheckApiResponse checks an API resonse matches the provded message 126 | func CheckApiResponse(status api.Response, code string) error { 127 | if status.Code != code { 128 | return fmt.Errorf("Incorrect API response code, expected: '%s' received: '%s'", code, status.Code) 129 | } 130 | 131 | return nil 132 | } 133 | 134 | func ParseAndCheckAPIResponse(resp *http.Response, code string) error { 135 | ar := api.Response{} 136 | 137 | err := ParseJson(resp, &ar) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | return CheckApiResponse(ar, code) 143 | } 144 | 145 | func (tc *Client) GetAPIResponse(path string, statusCode int, code string) error { 146 | resp, err := tc.Get(path, statusCode) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | return ParseAndCheckAPIResponse(resp, code) 152 | } 153 | 154 | // Post JSON to an api endpoint 155 | func (tc *Client) PostJSON(path string, statusCode int, requestInst interface{}) (*http.Response, error) { 156 | queryPath := tc.basePath + path 157 | 158 | js, err := json.Marshal(requestInst) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | resp, err := tc.Post(queryPath, "application/json", bytes.NewReader(js)) 164 | if err != nil { 165 | return resp, err 166 | } 167 | 168 | if resp.StatusCode != statusCode { 169 | return resp, fmt.Errorf("Incorrect status code from %s received: %d expected: %d", path, resp.StatusCode, statusCode) 170 | } 171 | 172 | return resp, nil 173 | } 174 | 175 | // PostForm Post a form to an api endpoint 176 | func (tc *Client) PostForm(path string, statusCode int, v url.Values) (*http.Response, error) { 177 | queryPath := tc.basePath + path 178 | 179 | resp, err := tc.Client.PostForm(queryPath, v) 180 | if err != nil { 181 | return resp, err 182 | } 183 | 184 | if resp.StatusCode != statusCode { 185 | return resp, fmt.Errorf("Incorrect status code from %s received: %d expected: %d", path, resp.StatusCode, statusCode) 186 | } 187 | 188 | return resp, nil 189 | } 190 | -------------------------------------------------------------------------------- /swagger.yml: -------------------------------------------------------------------------------- 1 | # AuthPlz Swagger API specification 2 | swagger: '2.0' 3 | info: 4 | title: AuthPlz API 5 | description: User Auth(entication | orization) and management microservice API 6 | version: "1.0.0" 7 | host: test.authplz.com 8 | schemes: 9 | - https 10 | basePath: /api/ 11 | produces: 12 | - application/json 13 | paths: 14 | 15 | /status: 16 | get: 17 | summary: User login status 18 | description: Endpoint to check user login status 19 | responses: 20 | 200: 21 | description: 22 | schema: 23 | $ref: '#/definitions/ApiResponse' 24 | 25 | /login: 26 | post: 27 | summary: Local login Endpoint 28 | description: Endpoint for local account login. 29 | responses: 30 | 200: 31 | description: Login success 32 | schema: 33 | $ref: '#/definitions/ApiResponse' 34 | 202: 35 | description: Further Authorization Required 36 | schema: 37 | $ref: '#/definitions/ApiResponse' 38 | 404: 39 | description: Unauthorized or user not found 40 | 429: 41 | description: Rate Limited 42 | default: 43 | description: Unexpected error 44 | schema: 45 | $ref: '#/definitions/ApiResponse' 46 | 47 | /logout: 48 | post: 49 | summary: Logout Endpoint 50 | description: Endpoint for local account login. 51 | responses: 52 | 200: 53 | description: Logout success 54 | schema: 55 | $ref: '#/definitions/ApiResponse' 56 | 401: 57 | description: Unauthorized 58 | default: 59 | description: Unexpected error 60 | schema: 61 | $ref: '#/definitions/ApiResponse' 62 | 63 | /account: 64 | get: 65 | summary: User Profile 66 | description: The User Profile endpoint returns information about the Uber user that has authorized with the application. 67 | responses: 68 | 200: 69 | description: Profile information for a user 70 | schema: 71 | $ref: '#/definitions/User' 72 | 401: 73 | description: Unauthorized 74 | schema: 75 | $ref: '#/definitions/ApiResponse' 76 | default: 77 | description: Unexpected error 78 | schema: 79 | $ref: '#/definitions/ApiResponse' 80 | 81 | /history: 82 | get: 83 | summary: User Activity 84 | description: Fetch previous account activity for a user 85 | parameters: 86 | - $ref: '#/parameters/offset' 87 | - $ref: '#/parameters/limit' 88 | responses: 89 | 200: 90 | description: History information for the given user 91 | schema: 92 | $ref: '#/definitions/UserEvents' 93 | default: 94 | description: Unexpected error 95 | schema: 96 | $ref: '#/definitions/Error' 97 | 98 | /me/tokens: 99 | get: 100 | summary: User 2fa tokens 101 | description: Fetch listing of regstered tokens for a given user 102 | responses: 103 | 200: 104 | description: List of tokens 105 | schema: 106 | $ref: '#/definitions/SecondFactors' 107 | default: 108 | description: Unexpected error 109 | schema: 110 | $ref: '#/definitions/Error' 111 | 112 | /me/oauth: 113 | get: 114 | summary: User OAuth connections 115 | description: Fetch delegate accounts for the logged in user 116 | responses: 117 | 200: 118 | description: List of active oauth connections 119 | default: 120 | description: Unexpected error 121 | schema: 122 | $ref: '#/definitions/Error' 123 | 124 | /me/apppw: 125 | get: 126 | summary: User Applicaton Passwords 127 | description: Fetch listing of existing application passwords for a given user 128 | responses: 129 | 200: 130 | description: List of active apps with app specific passwords 131 | default: 132 | description: Unexpected error 133 | schema: 134 | $ref: '#/definitions/Error' 135 | 136 | parameters: 137 | 138 | offset: 139 | name: offset 140 | in: query 141 | type: integer 142 | format: int32 143 | description: Offset the list of returned results by this amount. Default is zero. 144 | 145 | limit: 146 | name: limit 147 | in: query 148 | type: integer 149 | format: int32 150 | description: Number of items to retrieve. Default is 5, maximum is 100. 151 | 152 | 153 | definitions: 154 | ApiResponse: 155 | type: object 156 | properties: 157 | result: 158 | type: string 159 | description: API result, "ok" for success and "error" for failure 160 | message: 161 | type: string 162 | description: Message containing information about call success/failure 163 | 164 | User: 165 | type: object 166 | properties: 167 | ExtId: 168 | type: string 169 | description: External user ID 170 | Email: 171 | type: string 172 | description: User email address 173 | CreatedAt: 174 | type: string 175 | description: Account creation date 176 | LastLogin: 177 | type: string 178 | description: Last login time 179 | 180 | 181 | UserEvent: 182 | type: object 183 | properties: 184 | event_id: 185 | type: string 186 | description: Unique identifier for the user event. 187 | event_type: 188 | type: string 189 | description: Type of user event 190 | description: 191 | type: string 192 | description: Description of the user event. 193 | 194 | UserEvents: 195 | type: array 196 | items: 197 | $ref: '#/definitions/UserEvent' 198 | 199 | 200 | -------------------------------------------------------------------------------- /templates/activation.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | Hi {{.Username}}, 6 |

7 | Thanks for signing up to {{.ServiceName}}! To activate your account, please click here or copy the following link into the address bar: 8 |

9 | {{.ActionURL}} 10 |

11 | If you did no sign up for {{.ServiceName}}, no need to worry, just ignore this email. 12 |

13 | Thanks, 14 |

15 | The team at {{.ServiceName}} 16 |

17 | 18 | -------------------------------------------------------------------------------- /templates/loginnotice.tmpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authplz/authplz-core/31cb102d6c86006f824a7d6eab716e7f93f6f172/templates/loginnotice.tmpl -------------------------------------------------------------------------------- /templates/passwordchanged.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | Hi {{.Username}}, 6 |
7 | Your password for {{.ServiceName}} has been updated successfully. 8 |
9 | Thanks, 10 |
11 | The team at {{.ServiceName}} 12 |

13 | 14 | -------------------------------------------------------------------------------- /templates/passwordreset.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | Hi {{.Username}}, 6 |
7 | You requested a password reset for {{.ServiceName}}. To reset your password, please click here or copy the following link into the address bar: 8 |
9 | {{.ActionURL}} 10 |
11 | Please note this link will expire in 1 hour. If you did not request a password reset, no need to worry, just ignore this email. 12 |
13 | Thanks, 14 |
15 | The team at {{.ServiceName}} 16 |

17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /templates/unlock.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | Hi {{.Username}}, 6 |

7 | Your account for {{.ServiceName}} has been locked due to to an excess number of password attempts. 8 | To unlock your account, please click here or copy the following link into the address bar: 9 |

10 | {{.ActionURL}} 11 |

12 | If this isn't you, you may consider adding Multi-Factor Authentication for {{.ServiceName}}. 13 |

14 | Thanks, 15 |

16 | The team at {{.ServiceName}} 17 |

18 | 19 | --------------------------------------------------------------------------------